Интеллектуальные развлечения. Интересные иллюзии, логические игры и загадки.

Добро пожаловать В МИР ЗАГАДОК, ОПТИЧЕСКИХ
ИЛЛЮЗИЙ И ИНТЕЛЛЕКТУАЛЬНЫХ РАЗВЛЕЧЕНИЙ
Стоит ли доверять всему, что вы видите? Можно ли увидеть то, что никто не видел? Правда ли, что неподвижные предметы могут двигаться? Почему взрослые и дети видят один и тот же предмет по разному? На этом сайте вы найдете ответы на эти и многие другие вопросы.

Log-in.ru© - мир необычных и интеллектуальных развлечений. Интересные оптические иллюзии, обманы зрения, логические флеш-игры.

Привет! Хочешь стать одним из нас? Определись…    
Если ты уже один из нас, то вход тут.

 

 

Амнезия?   Я новичок 
Это факт...

Интересно

Имя верблюда, изображенного на пачке сигарет "Camel" - Старый Джо.

Еще   [X]

 0 

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

Как и все издания данной серии, эта книга адресована тем, кто хочет научиться делать с помощью уже знакомых программных пакетов новые интересные вещи. Издание будет полезно и новичкам, и опытным программистам. Автор описывает удивительные возможности, скрытые в языке, и на примерах учит читателя программистским фокусам – от «мышек-невидимок» и «непослушных окон» до воспроизведения MP3 и управления офисными программами Word и Excel из приложений Delphi. Купив эту книгу, вы пройдете непростой путь к вершинам программистского мастерства весело и интересно.

Год издания: 2010

Цена: 70 руб.



С книгой «Программирование в Delphi. Трюки и эффекты» также читают:

Предпросмотр книги «Программирование в Delphi. Трюки и эффекты»

Программирование в Delphi. Трюки и эффекты

   Как и все издания данной серии, эта книга адресована тем, кто хочет научиться делать с помощью уже знакомых программных пакетов новые интересные вещи. Издание будет полезно и новичкам, и опытным программистам. Автор описывает удивительные возможности, скрытые в языке, и на примерах учит читателя программистским фокусам – от «мышек-невидимок» и «непослушных окон» до воспроизведения MP3 и управления офисными программами Word и Excel из приложений Delphi. Купив эту книгу, вы пройдете непростой путь к вершинам программистского мастерства весело и интересно.


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

Введение

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

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

   Мы будем рады узнать ваше мнение!
   Все примеры, приведенные в книге, вы можете найти по адресу http://www.piter.com/download.
   На сайте издательства http://www.piter.com вы найдете подробную информацию о наших книгах.

Глава 1
Окна

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

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

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

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

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

   Примечание
   Здесь сказано, что функции изменяют цвет кнопки приложения на Панели задач. Однако этого не происходит при выполнении приведенных ниже примеров. Почему так получается и как это изменить, рассказано в следующем разделе этой главы (стр. 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 приложение, нуждающееся во внимании, не останется незамеченным.

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


   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).

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

   Из сказанного выше можно заключить, что отображение окна поверх других окон может пригодиться как минимум в двух случаях: для важных окон приложения (например, окна ввода пароля) и/или в случае, если значок приложения не выводится на Панели задач (как скрыть значок, было рассказано выше).
   После небольшого отступления можно рассмотреть способы, позволяющие задать положение формы так, чтобы другие окна не могли ее закрыть.
   Первый способ прост до предела: достаточно присвоить свойству 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.

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

   Кстати, простейшее оконное приложение, написанное на 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.

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

   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 приходит также при перерисовке так называемых «самоперерисовывающихся» (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.

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

   Не буду заострять внимание на регистрации класса главного окна приложения, так как она аналогична приведенной в листинге 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. Окно открытия файла
   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. Окно для выбора цвета
   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. Окно для выбора шрифта
   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. Окно для выбора папки
   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 – в противном случае.

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

Листинг 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.

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

   Рис. 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.

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

Листинг 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. Определение присутствия в системе мыши
   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. Эта функция не принимает аргументов и возвращает дескриптор окна, захватившего указатель ранее. С помощью этой функции можно, например, удостовериться, что захватом указателя мыши не нарушается работа другого приложения (что маловероятно).

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

   С функцией 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.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. Получение информации о клавиатуре
   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)
   

комментариев нет  

Отпишись
Ваш лимит — 2000 букв

Включите отображение картинок в браузере  →