Текст
                    А.Я. Архангельский
Разработка прикладных
программ для Windows
в Delphi 5
Реализация требований
к программам Windows
средствами Delphi
Интернационализация
прикладных программ
Технологии перетаски-
вания объектов
Drag&Drop и Drag&Doc
Текстовые и графичес-
кие редакторы
Проигрыватели

BCEODOJ ’Hi
А.Я. Архангельский Разработка прикладных программ для Windows в Delphi 5 Москва ЗАО «Издательство БИНОМ» 1999
УДК 004.43 ББК 32.973-018.1 А87 Архангельский А. Я. I Разработка прикладных программ для Windows в Delphi 5 — М.: ЗАО «Издательство БИНОМ», 1999. — 256 с.; ил. (Все о Delphi). В книге рассматривается методика построения прикладных программ для I Windows с помощью Delphi 5 и более ранних версий Delphi Излагаются требования, которым должна удовлетворять любая программа для Windows, и способы удовлетворения этим требованиям средствами Delphi Обсуждаются средства интернационализации прикладных программ Рассматривается пост- роение приложений с множеством форм, текстовых и графических редакго- I ров, приложений мультимедиа, основы мультипликации, печати текстов и изображений. Излагаются технологии перетаскивания объектов Drag&Drop и Drag&Doc. Обсуждаются вопросы обеспечения бессбойной работы с помощью механизма исключений. Анализируются проблемы развертывания прикладных программ, их установки и настройки. Приводятся примеры создания прик- I ладных программ. Методика работы рассматривается для разных версий Del- phi, включая Delphi 1. которая не потеряла актуальности и в настоящее время. Книга рассчитана как ия начинающих пользователей, так и на опытных разработчиков. ISBN 5-7989-0153-Х © Архангельский А.Я., 1999 © ЗАО «Издательство БИНОМ». 1999
Содержание От автора . ..................... ....... 9 Глава 1. Требования к интерфейсу пользователя приложений для Windows и их реализация в Delphi 5................................11 1.1 Общие рекомендации по разработке графического интерфейса...............-..............11 1.2 Многооконные приложения ... ........12 1.3 Стиль окон приложения. . . _ . ............13 1-4 Цветовое решение приложения . ............20 1.5 Шрифты текстов.............................. ... 23 1.6 Меню........................................ 24 1.6.1 Требования к меню в приложении Windows. .....24 1.6.2 Методика проектирования меню и инструментальной панели.........................................28 1.7 Компоновка форм, применение фреймов, создание иерархии форм и фреймов.................... .36 1.8 Последовательность фокусировки элементов.........43 1.9 Подсказки и контекстно-зависимые справки.........46 1.10 Обеспечение бессбойной работы приложений. . 54 1.10.1 Исключения и способы борьбы с ними. .......54 1.10.2 Не допускать исключений.....................57 1.10.3 Защита кода зачистки........-...............58 1.10.4 Обработка исключений в блоках try... except ...... 60 1.10.4.1 Синтаксис блоков try — except и операторов on...do . . . 60 1.10.4.2 Последовательность обработки исключений.63 1.11 Печать из приложения текстов и изображений. ... 64 1.11.1 Печать с помощью функций файлового ввода/вывода. . . 64 1.11.2 Печать форм методом Print...................65 1.11.3 Печать методом Print текстов в обогащенном формате . . 65 1.11.4 Печать файлов средствами стандартных приложений Windows с помощью функции ShellExecute......... . 66 1.11.5 Печать с помощью объекта Printer............66
' 6 Разработка прикладных программ для Window» в Delphi 5 Глава 2. Проектирование окон с изменяемыми размерами...................................... = . 69 2.1 Выравнивание компонентов — свойство Align - 69 2.2 Изменение местоположения и размеров компонентов..................................... .72 2.3 Панели с перестраиваемыми границами...........75 2.4 Ограничение пределов изменения размеров окон и компонентов...................... 76 2.5 Масштабирование компонентов ................ . 78 Глава 3. Обработка событий клавиатуры и мыши ..... 81 3.1 События мыши..................................81 3.1.1 Последовательность событий................81 3.1.2 Распознавание источника события, нажатых кнопок и клавиш, координат курсора................85 3.2 События клавиатуры.......................... 89 3.2.1 Последовательность событий . 89 3.2.2 Распознавание нажатых клавиш............ .... 91 Глава 4. Перетаскивание объектов. ........ 97 4.1 Перетаскивание информации об объектах — технология Drag&Drop....................97 4.2 Перетаскивание и встраивание объектов — технология Drag&Doc. Плавающие окна...............101 4.3 Буксировка компонентов в окне приложения . . . 111 Глава 5. Формы....................................119 5.1 Управление формами...........................119 5.2 Модальные формы........................... . . 124 5.3 Пример приложения с модальными формами заставки и запроса пароля.................126 5.4 Управление формами в приложениях с интерфейсом множества документов (приложениях MDI)............131 5.5 Пример приложения с интерфейсом множества документов — простой многооконный редактор ... 134 5.6 Объект Screen и приложения, работающие с нескольким мониторами...........................137
Содержание Глава 6. Графика и мультимедиа.............................143 6.1 Построение графических изображений.....................143 6.1.1 Использование готовых графических файлов..........143 6.1.1.1.Компонент linage и некоторые его свойстве.- - 143 6.1.1-2 Простое приложение для просмотра графических файлов................................................145 6.1.1.3 Форматы графических файлов...................147 6-1.1,4 Классы для хранения графических объектов TPicture, TBitMap. TIcon и TMetafile ... 148 6.1.2 Редактор Изображений Image Editor.................150 6.1.2.1 Создание файла изображения................. 150 6.1.2.2 Создание пиктограммы для шаблона компонента в библиотеке. .............................- .... 153 6.1.2.3 Создание пиктограммы для кнопки..............154 6.1.2.4 Работа с файлами ресурсов.............. ....156 6.1.3 Канва — холст для рисования........ ... 157 6.1.3.1 Канва и пиксели.................. . . . . 157 6.1.3.2 Рисование по пикселям........................158 6.1.3.3 Рисование с помощью пера Реп.................150 6.1.3.4 Brush — кисть.............................. 164 6.1.4 Пример построения собственного простого графического редактора................... - . 166 6.1.5 Режимы рисования............................ ... 172 6.1.5.1 Режимы пера.......................- . 172 6.1.5,2 Режимы копирования и рисования канвы. ... .172 6.1.6 Продолжение создания собственного графического редактора.......................... ... 175 6.1.7 События OnPaint ........ 183 6.2 Мультимедиа и анимация . . 185 6.2.1 Звук ....................................... .185 6.2.1.1 Типы звуковых и мультимедиа файлов........ . 185 6.2.1.2 Процедуры воспроизведения вауков Веер, MessageBeep и PlaySound................... ... 187 6.2.2 Начала анимации — создание собственной мультипликации.................-............. ...191 6.2.3 Воспроизведение немых видео клипов — компонент Animate.......................... . 200 6.2.4 Универсальный проигрыватель MediaPlayer . . . 205 Глава 7. Развертывание приложении . ............211 7.1 Интернационализация приложений.............211 7-2 Автономные приложения и пакеты............ 217 7.3 Установка и настройка приложения; работа с системным реестром.....................222
8 Разрсботка прикладных прогромм для Windows Delphi 5 7.4 Установка и настройка приложения: работа с файлами -INI .... . 228 Приложение.......................... . . ... 233 П.1 Код текстового редактора ... - - . . 233 П.2 Код графического редактора . . .... 242
От автора Эта книга посвящена методике разработки прикладных про- грамм (приложений) для Windows средствами системы объект- но-ориентированного визуального программирования Delphi 5. Delphi — популярная система, которая позволяет даже людям, не очень искушенным в программировании, создавать приклад- ные программы для Windows, удовлетворяющие самым жестким требованиям к подобным программам. Эти возможности связаны с тем, что большинство требований уже учтено разработчиками Delphi, заложено в компонентах библиотеки и автоматически пе- реносится в ваше приложение. Тем не менее, каждый разработ- чик прикладных программ должен представлять, каким требова- ниям должна удовлетворять любая прикладная программа, рабо- тающая в среде Windows, и как использовать для этого богатей- шие возможности, которые заложили в Delphi ее создатели из фирмы Borland. Именно этим вопросам и посвящена данная кни- га. Книга является третьей, выпущенной в серии «Все о Delphi». В двух первых рассматривалась методика работы со средой раз- работки Delphi 5 и библиотека компонентов. Данная книга явля- ется совершенно самостоятельным изданием и для ее понимания не обязательно знать материал первых двух книг серии. Однако, при ее написании я предполагал, что какие-то минимальные тех- нические навыки работы со средой разработки Delphi читатель имеет. Эти сведения могут быть почерпнуты просто из опыта или в результате изучения любой книги о Delphi 5. Кроме того, не- плохо представлять свойства и методы наиболее часто используе- мых компонентов библиотеки. Хотя в книге везде даются поясне- ния применяемых средств и методов, более глубокие сведения о компонентах позволят, конечно, лучше использовать заложен- ные в них возможности. Изложение методики работы проводится параллельно для различных версий Delphi, включая Delphi 1. Дело в том, что Del- phi 1 — единственная версия, позволяющая строить приложения для любых, в том числе и маломощных 16-разрядных компьюте- ров. А во многих производственных задачах используются имен- но такие компьютеры — более мощные тзм просто ие рентабель- ны. Кроме того ориентация на любые версии Windows, включая и Windows 3.x, требуется при работе в сетях, содержащих раз- ные, в том числе и слабые компьютеры с малой дисковой и опера- тивной памятью. Подобные задачи можно легко решать с помо- щью Delphi 1, которая именно поэтому распространяется фирмой Borland наряду с новейшими версиями. В то же время из-за како-
10 Рааработко прикладных прогреми для Windows и Dolph! 5 го-то неоправданного снобизма практически все авторы литера- туры о Delphi забыли о Delphi 1. Так что данная книга восполня- ет, в частности, и этот пробел. В книге рассмотрены как общие вопросы, присущие практи- чески любым прикладным программам для Windows, так и ха- рактерные приемы построения текстовых и графических редак- торов, плееров мультимедиа и других разновидностей приложе- ний. Не затрагиваются специфические методики построения приложений для работы с базами данных и Интернет. Эти вопро- сы очень обширны, требуют углубленного изучения и их невоз- можно было изложить в рамках данной книги из-за естественно- го ограничения объема. Этим классам прикладных программ бу- дут посвящены отдельные книги серии «Все о Delphi». Я пытался писать книгу так, чтобы она представляла интерес и для начинающих, и для опытных пользователей. Начинающие найдут в ией детальную, расписанную по шагам методику по- строения прикладных программ различного назначения и нема- ло советов, способствующих хорошему стилю программирова- ния. Надеюсь, что и опытные пользователи почерпнут в ней и в рассмотренных примерах немало нового для себя, особенно в тех- нологиях, развитых в последних версиях Delphi, включая техно- логию Drag&Doc, инструментарий интернационализации прило- жений и многое другое.
Глава 1 Требования к интерфейсу пользователя приложений для Windows и их реализация в Delphi 5 1.1 Общие рекомендации по разработке графического интерфейса Под графическим интерфейсом пользователя (Graphical User Interface — GUI) подразумевается тип экранного представления, при котором пользователь может выбирать команды, запускать задачи и просматривать списки файлов, указывая на пиктограм- мы или пункты в списках меню, показанных на экране. Дейст- вия могут, как правило, выполняться с помощью мыши, либо на- жатием клавиш на клавиатура. Типичным примером графиче- ского интерфейса пользователя является Windows 95/98- Delphi предоставляет разработчику приложения широкие воз- можности быстрого и качественного проектирования графиче- ского интерфейса пользователя — различных окон, кнопок, меню и т.д. Так что разработчик может в полной мере проявить свою фантазию. Но полеты фантазии очень полезно ограничи- вать. Есть определенные принципы построения графического ин- терфейса пользователя, и пренебрегающий ими обречен на то, что его приложение будет выглядеть чужеродным объектом в среде Windows. Для пользователя одним из принципиальных преимуществ работы с Windows является то, что большинство имеющихся приложений выглядят и ведут себя сходным образом. После того, как вы поработаете с несколькими приложениями, вы обна- ружите, что можете заранее почти наверняка сказать, где можно найти ту или иную функцию в программе, которую только что приобрели, или какие быстрые клавиши надо использовать для выполнения тех или иных операций- Фирма Microsoft предложила спецификации для разработки программного обеспечения Windows, направленные на то, чтобы пользователь не тратил время на освоение нюансов пользователь- ского интерфейса новой программы, чтобы он смог как можно скорее продуктивно применять ваше приложение- Эти специфи- кации образуют основу программы логотипа Win95, проводимой
13 Глава 1 Microsoft. Чтобы вы могли поставить на свой программный про- дукт штамп «Разработано для Windows 95/98ь, ваша программа должна удовлетворять определенным критериям. Когда вы види- те этот логотип на каком-то изделии, аппаратном или програм- мном, вы можете быть уверены, что оно будет работать нормаль- но в среде Windows 95/98. Конечно, вряд ли вы будете очень озабочены приобретением официального права на логотип Windows, если только не разра- ботали сногсшибательную программу широкого применения, ко- торую надеетесь успешно продавать на международном рынке. Но прислушаться к рекомендациям по разработке графического интерфейса пользователя в любом случае полезно. Они основаны на психофизиологических особенностях человека и существенно облегчат жизнь будущим пользователям вашей программы, уве- личат производительность их работы. Помимо общих рекомендаций по разработке графического ин- терфейса пользователя полезно также поддерживать с первых шагов разработки приложения контакт со своими заказчиками или будущими пользователями. С этой точки зрения не стоит сразу браться за разработку всего приложения. Полезно сначала построить его прототип, отражающий предлагаемый вами графи- ческий интерфейс, за которым на первых порах не скрывается реальных программ. Этот прототип можно обсудить с заказчика- ми и пользователями и только после их одобрения приступать к действительной реализации приложения. 1.2 Многооконные приложении Чаще всего сколько-нибудь сложное приложение не может ограничиться одним окном. Поэтому прежде всего вам нужно ре- шить вопрос управления окнами. Есть две различные модели приложений: с интерфейсом одного документа (SDI) и с интер- фейсом множества документов (MDI). В большинстве случаев следует отдавать предпочтение интер- фейсу SDI. Этот интерфейс не обязательно предполагает наличие действительно только одного окна, как в приложениях Windows, типа «Калькулятор». Такое приложение, как «Проводник» Win- dows, также является SDI приложением, но в нужные моменты оно создает вторичные окна для поиска файлов или папок, зада- ния параметров, просмотра свойств файлов и других целей. С другой стороны, у приложений MDI тоже есть свои преиму- щества. Хороший пример такого приложения — Microsoft Word. В приложении MDI имеется родительское (первичное) окно и ряд
Требовании к интерфейсу приложений для Windows 13 дочерних окон (называемых также окнами документов). Быва- ют ситуации, когда выгодно отображать информацию в несколь- ких окнах, которые совместно используют элементы интерфейса (например, меню или инструментальные линейки). Окна доку- ментов управляются и ограничиваются родительским окном. Если вы уменьшаете размер родительского окна, то дочерние окна могут исчезать из поля зрения. Случаи, когда нужно использовать модель MDI, довольно ред- ки. Прежде всего, это следует делать только тогда, когда все до- черние окна содержат идентичные объекты — например, тексто- вые документы или электронные таблицы. Не применяйте MDI, если вы собираетесь работать в приложении с дочерними окнами разного типа (например, текстовыми документами и электрон- ными таблицами одновременно). Не применяйте MDI, если вы хотите управлять тем, какое из дочерних окон должно находить- ся поверх других, используя свойство «всегда наверху», или если вы хотите управлять размерами окон, делать их невидимыми и т. п. Интерфейс MDI предназначен для очень узкого диапазона приложений, в которых все дочерние окна однородны (как это имеет место в Word или Excel). Приспособить его к чему-то дру- гому не получится. Наконец, следует заметить, что Microsoft не поощряет разработку новых приложений MDI (в основном пото- му, что для предыдущих версий Windows было написано слиш- ком много плохих программ этого типа). Подробнее о технологии построения приложений MDI будет рассказано в разделе 5.4. 1.3 Стиль окон приложения Основным элементом любого приложения является форма — контейнер, в котором размещаются другие визуальные и невизу- альные компоненты. С точки зрения пользователя форма — это окно, в котором он работает с приложением. Каждой новой фор- ме, вводимой в приложение, соответствует свой модуль (unit), описывающий эту форму как класс и включающий, если необхо- димо, какие-то дополнительные константы, переменные, функ- ции и процедуры. К внешнему виду окон в Windows предъявляются определен- ные требования. К счастью, Delphi автоматически обеспечивает стандартный для Windows вид окон вашего приложения. Но вам надо продумать и указать, какие кнопки в полосе системного меню должны быть доступны в том или ином окне, должно ли окно допускать изменение пользователем его размеров, каким должен быть заголовок окна. Все эти характеристики окон обес- печиваются установкой и управлением свойствами формы.
14 Свойство BorderStyle определяет общий вид окна и операции с ним, которые разрешается выполнять пользователю. Это свой- ство может принимать следующие значения: bsSizeable Обычный вид окна Windows с полосой заголовка, с возможностью для пользователя изменять размеры окна с помощью кнопок в полосе заголовка или с по- мощью мыши, потянув за какой-либо край окна. Это значение BorderStyle задается по умолчанию bsDialog Неизменяемое по размерам окно. Типичное окно диалогов bsSingle Окно, размер которого пользователь не может изме- нить, потянув курсором мыши край окна, но может менять кнопками в полосе ваголовна bsToolWindow То же. что bsSingle, но с полосой ваголовка меньше- го размера bsSizeToolWin То же, что bsSizeable, но с полосой заголовка мень- шего размера и с отсутствием в ней кнопок измене- ния размера bsNone Без полосы заголовка. Окно ве только не допускает изменения размера, но и не позволяет переместить его по экрану Свойство Bordericons определяет набор кнопок, которые име- ются в полосе заголовка. Множество кнопок задается элемента- ми: bySistemMenu кнопка системного меню (для Windows 95/98 и NT это кнопка с крестиком, закрывающая окно) byMinimiae byMaximiae byHelp кнопка Свернуть, сворачивает окно до пиктограммы кнопка Развернуть, разворачивает окно на весь экран кнопка справки Следует отметить, что не все кнопки могут появляться при любых значениях BorderStyle. На рис. 1.1 представлен вид окон форм во время выполнения при некоторых сочетаниях свойств BorderStyle и Bordericons. Для создания диалоговых окон обычно используется стиль заго- лоака bsDialog (формы Form3 и Form4 на рис. 1.1), причем в этих окнах можно исключить кнопку системного меню (форма Form4) и в этом случае пользователь ие может закрыть окно ни-
Требовании к интерфейсу пользователи приложений для Windows 15 какими способами, кроме как выполнить какие-то предписанные ему действия на этой форме. При стиле bsNone пользователь не может изменить ни размер, ни положение окна на экране. Фор- мы Form3, Form6 и FormS внешне различаются только размером полосы заголовка, уменьшенным в двух последних формах. Но между ними есть и принципиальное различие: размер формы FormS (стиль заголовка bsSizeToolWin) пользователь может из- менить, потянув курсором мыши за край окна. Для форм Form3 и Forra8 это невозможно. Обратите также внимание, что в фор- мах FormS и Fonn8 задание кнопок свертывания и развертыва- ния окна никак не влияет на его вид: эти кнопки просто не могут появляться в этих стилях полос заголовков окон. Рис. 1.1 Формы при розных сочетаниях свойств BorderStyte и Bordertcons Для основного окна приложения с неизменяемыми размерами наиболее подходящий стиль — BorderStyle = bsSingle с исключением из числа доступ- ных кнопок кнопки Развернуть (Borderlcons.byMoximize = false). Это позво- лит пользователю сворачивать окно, восстанавливать, но не даст возможно- сти развернуть окно на весь экран или изменить размер окно
16 Глане 1 Свойство формы Windowstate определяет вид, в котором окно первоначально предъявляется пользователю при выполнении приложения. Оно может принимать значения: wsNormal нормальный вид окна (это значение WbidowSta- te используется по умолчанию) wsMinimized окно свернуто wsMaximized окно развернуто на весь экран Если свойство WindowState имеет значение wsNormal или пользователь, манипулируя кнопками в полосе заголовка окна, привел окно в это состояние, то положение окна при запуске при- ложения определяется свойством Position, которое может прини- мать значения: poD esigned poScreenCenter Первоначальные размеры и положение окна во время выполнения те же, что во время проекти- рования. Это значение принимается по умолча- нию, но обычно его следует изменить Окно располагается в центре экрана. Размер окна тот, который был спроектировен. В мультиэкран- ных приложениях (см. раздел 5.6), работающих одновременно с множеством мониторов (только начиная с Delphi 4) эта центральная позиция мо- жет быть несколько изменена, чтобы изображе- ние попало точно на один монитор, определяе- мый свойством DefauItMonitor
Требосоннм к интерфейсу пользователя чриложанмй для Windows 17 poDesktopCenter Это значение предусмотрено только начиная с poDefault Delphi 4. Окно располагается в центре экрана. Размер окна тот, который был спроектирован. Этот режим не приспосабливается к приложени- ям с множеством мониторов (см. раздел 5.6) Местоположение и размер окна определяет Win- dows, учитывая размер и разрешение экрана. При последовательных показах окна его положе- ние сдвигается немного вниз и вправо poDefauliPosOnly Местоположение окна определяет Windows. При последовательных покавех окна его положение сдвигается немного вниз и вправо. Размер окна — спроектированный poDefaiiltSi-zeOnly размер окна определяет Windows, учитывая раз- мер и разрешение экрана. Положение окна — спроектированное poMainFormCenter Это значение предусмотрено только начиная с Delphi 5. Окно располагается в центре главной фор- мы. Размер окна тот, который был спроектирован. Этот режим не щжспосабливаатся к приложениям с множеством мониторов (см. раздел 5.6). Исполь- зуется только для вторичных форм. Для главной формы действует так же, как poScreenCenter Обычно целесообразно для гловной формы приложения зодавоть зночение Position равным poScreenCenter или poDefoult. И только в сравнительно редких случаях, когда но экране при выполнении приложения должно опре- деленным оброзом располагаться несколько окон, имеет смысл оставлять значение poDesigned, принимаемое по умолчанию. Если выбранное значение свойства Position предусматривает выбор размера формы самим Windows по умолчанию, то на этот выбор влияют свойства PixelsPerlnch и Scaled. По умолчанию первое из них задается равным количеству пикселей на дюйм в системе, второе установлено в false. Если задать другое число пикселей на дюйм, то свойство Scaled автоматически становится равным true. В этом случае при запуске приложения размер фор- мы будет изменяться в соответствии с пересчетом заданного числа пикселей на дюйм к реальному числу пиксепей на дюйм в системе (но только при разрешающем это значении свойства Position). Свойство Auto Scroll определяет, будут ли на форме в процессе выполнения появляться автоматически полосы прокрутки в слу-
чае, если при выбранном пользователем размере окна не все ком- поненты помещаются в нем. Если значение AutoScroll равно true, то будут. В противном случае при уменьшении размера окна пользователь теряет доступ к компонентам, не поместив- шимся в его поле. Свойство Icon задает пиктограмму формы. По умолчанию ис- пользуется стандартная пиктограмма Delphi. Нажав в Инспекто- ре Объектов кнопку с тремя точками в строке свойства Icon, вы попадаете в окно Редактора Изображений (Picture Editor), пред- ставленное на рис. 1.2. Щелкнув в нем на кнопке Load (загру- зить), вы можете выбрать любой файл с изображением пиктог- раммы (файл с расширением .ico). С Delphi поставляется некото- рое число пиктограмм, расположенных в каталоге lmoges\lcons. Рис. 1-2 Окно Редактора Изображений Свойство Icon задает только пиктограмму формы, которая отображается в левом иерхнем углу окна приложения в его нор- мальном состоянии. Но если пользователь свернет окно, то в по- лосе задач будет видна другая пиктограмма — пиктограмма при- ложения. Ту же пиктограмму увидит пользователь, если будет просматривать средствами Windows содержимое каталога. По умолчанию для нее используется стандартная пиктограмма Del- phi. При свертывании приложения рядом с пиктограммой в по- лосе задач пользователь будет видеть надпись — по умолчанию это имя приложения. Если вы хотите, то можете изменить эту пиктограмму и эту надпись. Для этого вы должны выполнить команду Project | Options и в открывшемся окне опций проекта пе- рейти на страницу Application (рис. 1.3). В этом окне вы можете задать заголовок (Title), который увидит пользователь в полосе за-
Требования к интерфейсу пользователя припожанин для Window» дач при сворачивании приложения. А кнопка Load Icon позволяет взм выбрать пиктограмму, которая будет видна в полосе задач при сворачивании приложения или при просмотре пользовате- лем каталога, в котором расположен выполняемый файл прило- жения. Одно из основных свойств формы — FormStyle, которое мо- жет принимать значения: fsNormal fsMDIFonn fsMDIChild fsStayOnTop Окно обычного приложения. Это значенне FormStyle принято по умолчанию Родительская форма приложения MDI, т.е. приложе- ния с дочерними окнами, используемого при работе с несколькими документами одновременно Дочерняя форма приложения MDI Окно, остающееся всегда поверх остальных окон Win- dows О приложениях MDI подробно будет рассказано в разделе 5.4. Так что значения fsMDIForm и fsMDIChild мы пока подробнее обсуждать не будем. А значение FormStyle = fsStayOnTop делает окно всегде остающимся на экране поверх остальных окон не то- лько данного приложения, но и всех других приложений, в кото- рые может перейти пользователь.
20 Глот Используйте стиль FormStyle = fsStayOnTop для отображения окон сооб- щений пользователю о каких-то аварийных ситуациях. В ряде случаен полезно предоставить пользователю самому ре- шать, сделать ли данное окно располагающимся всегда поверх остальных, или нет. Например, ему может временно потребова- ться перейти в какое-то другое приложение, чтобы получить не- обходимую информацию» и при этом будет хотеться видеть по- верх этого приложения ваше окно, чтобы сравнивать в этих двух окнах какие-то данные. Такую возможность легко предоставить пользователю. Введите в меню вашего окна (рис. 1.4) раздел По- верх остальных (о меню подробнее будет рассказано в разделе 1.6) и в обработчик щелчка на этом разделе вставьте операторы MStayOnTop.Checked := not MStayOnTop.Checked; if MStayOnTop.Checked then Forml.FormStyle fsStayOnTop else Forml.FormStyle := fsNormal; В этом коде подразумевается, что объект раздела меню, о ко- тором идет речь, назван MStayOnTop. Тогда при выборе пользо- вателем этого раздела меню в нем появится индикатор (см. рис. 1.4), а окно приобретет статус расположенного всегда поверх остальных. При повторном выборе этого раздела индикатор ис- чезнет и окно приобретет обычный статус. Рис. 1.4 Окно с переключающимся статусом 1.4 Цветовое решение приложения Цвет является мощным средством воздействия на психику че- ловека. Именно поэтому обращаться с ним надо очень осторож- но. Неудачное цветовое решение может приводить к быстрому утомлению пользователя, работающего с вашим приложением, к рассеиванию его внимания, к частым ошибкам. Слишком яркий или неподходящий цвет может отвлекать внимание пользовате- ля или вводить его в заблуждение, создавать трудности в работе. А удачно подобранная гамма цветов, осмысленные цветовые ак-
Требомиия к интерфейсу польаовпталя приложении для Window»21 центы снижают утомляемость, сосредоточивают внимание поль- зователя на выполняемых в данный момент операциях, повыша- ют эффективность работы. С помощью цвета вы можете на что-то намекнуть ил! привлечь внимание к определенным областям эк- рана. Цвет может также связываться с различными состояниями объектов. Надо стремиться использовать ограниченный набор цветов и уделять внимание их правильному сочетанию. Расположение яр- ких цветов, таких, как красный, на зеленом или черном фоне за- трудняет возможность сфокусироваться на них. Не рекомендует- ся использовать дополнительные цвета. Обычно наиболее прием- лемым цветом для фона будет нейтральный цвет, например, свет- ло-серый (используется в большинстве продуктов Microsoft). По- мните также, что яркие цвета кажутся выступающими из плос- кости экрана, в то время как темные как бы отступают вглубь. Цвет не должен использоваться в качестве основного средства передачи информации. Можно использовать различные панели, формы, штриховку и другие методики выделения областей экра- на. Microsoft даже рекомендует разрабатывать приложение сна- чала в черно-белом варианте, а уже потом добавлять к нему цвет Нельзя также забывать, что восприятие цвета очень индиви- дуально. А по оценке Microsoft девять процентов взрослого насе- ления вообще страдают нарушениями цветовосприятия. Поэтому не стоит навязывать пользователю свое видение цвета, даже если оно безукоризненно. Надо предоставить пользователю возмож- ность самостоятельной настройки на наиболее приемлемую для него гамму. К тому же не стоит забывать, что может быть кто-то захочет использовать вашу программу на машине с монохром- ным монитором. Посмотрим теперь, как задаются цвета приложения, разраба- тываемого в Delphi. Большинство компонентов имеют свойство Color (цвет) типа TColor, который вы можете изменять в Инспек- торе Объектов при проектировании или программно во время вы- полнения (если хотите, чтобы цвета в различных режимах рабо- ты приложения были разные). Щелкнув на этом свойстве в Инс- пекторе Объектов, вы можете увидеть в выпадающем списке бо- льшой набор предопределенных констант, обозначающих цвета. Все их можно разбить на две группы: статические цвета типа clBIack — черный, clGreen — зеленый и т.д., и системные цвета типа clWindow — текущий цвет фона окон, cIMcnuText — теку- щий цвет текста меню и т.д. Статические цвета вы выбираете сами и они будут оставаться неизменными при работе приложения на любом компьютере. Это
22 ие очень хорошо, поскольку пользователь не сможет адаптиро- вать вид вашего приложения к своим потребностям. При выборе желательной ему цветовой схемы пользователь может руководст- воваться самыми разными соображениями: начиная с практиче- ских (например, он может хотеть установить черный фон, чтобы экономить энергию батареи), и кончая эстетическими (он может предпочитать, например, шкалу оттенков серого, потому что не различает цвета). Все это он не может делать, если вы задали в приложении статические цвета. Но уж если по каким-то сообра- жениям вем надо их задать, старайтесь использовать базовый на- бор из 16 цветов. Если вы попытаетесь использовать 256 (или, что еще хуже, 16 миллионов) цветов, это может замедлить работу вашего приложения, или оно будет выглядеть плохо на машине пользователя с 16 цветами. К тому же подумайте (а, как прави- ло, это надо проверить и экспериментально), как будет выгля- деть ваше приложение на монохромном дисплее. Исходя из изложенных соображений, везде, где это имеет смысл, следует использовать для своего приложения палитру си- стемных цветов. Это те цвета, которые устанавливает пользова- тель при настройке Windows. Когда вы создаете новую форму или размещаете на ней компоненты, Delphi автоматически при- сваивает им цвета в соответствии со схемой цветов, установлен- ной в Windows. Конечно, вы будете менять эти установки по умолчанию. Но если при этом вы используете соответствующие константы системных цветов, то, когда пользователь изменит цветовую схему оформления экрана Windows, ваше приложение также будет соответственно меняться и ие будет выпадать из об- щего стиля других приложений. Не злоупотребляйте в приложении яркими цветами. Пестрое приложение — обычно признак дилетантизме разработчика, утомляет пользователя, рас- сеивает его внимание. Как правило, используйте системные цвета, которые пользователь может перестраивать по своему усмотрению. Из статических цветов обычно имеет смысл использовать только cIBIock — черный, cl White — белый и cIRed — красный цвет предупреждения об опосиости. Иногда, особенно для графики, предопределенных констант цвета ие хватает. Вам могут понадобиться такие оттенки, кото- рых нет в стандартных палитрах. В этом случае можно задавать цвет 4-байтовым шестнадцатеричным числом, три младших раз- ряда которого представляют собой интенсивности синего, зелено- го и красного цвета соответственно. Например, значение
Требования к интерфейсу лользсвчталя приложений для Windows 23 SOOFFOOOO соответствует чистому синему цвету, SOOOOFFOO — чис- тому зеленому, $OOOOOOFF — чистому красному. $00000000 — чер- ный цвет, SOOFFFFFF — белый. Единству цветового решения отдельных частей экрана способ- ствует использование свойства ParentColor. Если это свойство установлено в true, то цвет компонента соответствует цвету со- держащего его контейнера или формы. Это обеспечивает единст- во цветового решения окна и, кроме того, позволяет программно изменять цвет сразу группы компонентов, если вы, например, хотите, чтобы их цвет зависел от текущего режима работы при- ложения. Для такого группового изменения достаточно изменить только цвет контейнера. 1.5 Шрифты текстов Шрифт надписей и текстов компонентов Delphi задается свой- ством Font, имеющим множество подсвойств. Кроме того, в ком- понентах имеется свойство ParentFont. Если это свойство уста- новлено в true, то шрифт данного компонента берется из свойст- ва Font его родительского компонента — панели или формы, на которой расположен компонент. Использование свойств Parent- Font и ParentColor помогает обеспечить единообразие отображе- ния компонентов в окне приложения. По умолчанию для всех компонентов Delphi задается имя шрифта MS Sans Serif и размер — 8 (в Delphi 1 задается имя шрифта System и размер 10). Константа множества символов Charset задается равной DEFAULT_CHARSET. Последнее озна- чает, что шрифт выбирается только по его имени и размеру. Если описанный шрифт недоступен в системе, то Windows заменит его другим шрифтом. Чаще всего эти установки по умолчанию можно не изменять. Конечно, никто не мешает задать для каких-то компонентов дру- гой размер шрифта или атрибуты типа полужирный, курсив и т.д. Но изменять имя шрифта для вашего приложения надо с определенной осторожностью- Дело в том, что шрифт, установ- ленный на вашем компьютере, ие обязательно должзн иметься и на компьютере пользователя. Поэтому использование какого-то экзотического шрифта может привести к тому, что пользователь, запустив ваше приложение на своем компьютере, увидит вместо русского тексте абракадабру на никому не понятном языке. Что- бы избежать таких казусов, вам придется прикладывать к свое- му приложению еще и файлы использованных шрифтов и пояс- нять пользователю, как он должен установить их на своем
24 Глава 1 компьютере, если они там отсутствуют. Или вводить автоматиче- скую проверку и установку нужных вам шрифтов в установоч- ную программу вашего приложения. Использование шрифтов по умолчанию; System или MS Sans Serif, чаще всего позволяет избежать подобных неприятностей. Впрочем, увы, не всегда- Если вы используете для надписей рус- ские тексты, то при запуске приложения не компьютере с иеру- сифицированным Windows иногда возможны неприятности. Для подобных случаев все-таки полезно приложить файлы использо- ванных шрифтов к вашей программе. Вы можете при установке вашего приложения узнать, имеется ли на компьютере пользова- теля нужный шрифт, например, с помощью следующего кода: if (Screen.Fonts-IndexOffArial") - -1) then ... В этом коде многоточием обозначены действия, которые надо выполнить, если нужного шрифта (в примере — Arial) на компь- ютере нет. Эти действия могут заключаться в копировании фай- лов шрифта с установочной дискеты няи CDROM на компьютер пользователя. Другой выход из положения — ввести в приложение команду выбора шрифта пользователем. Это позволит ему выбрать подхо- дящий шрифт из имеющихся в его системе. Осуществляется по- добный выбор с помощью стандартного диалога, оформленного в виде компонента FontDialog. Код, обеспечивающий этот диалог, следующий: if (FontDialogl-Execute) then Font.Assign(FontDialogl.Font); Проведенную пользователем установку можно запоминать в файле -INI, в реестре или в файле конфигурации и читать авто- матически информацию из этого файла при каждом запуске при- ложения (см. разделы 7.3 и 7.4). 1.6 Меню 1.6.1 Требования к меню в приложении Windows Практически любое приложение должно иметь меню, поско- льку именно меню дает наиболее удобный доступ к функциям программы. Существует несколько различных типов меню: глав- ное меню с выпадающими списками разделов, каскадные меню, в которых разделу первичного меню ставится в соответствие спи- сок подразделов, и всплывающие или контекстные меню, появ- ляющиеся, если пользователь щелкает правой кнопкой мыши на каком-то компоненте.
Требовотя к интерфейсу пользователя приложении для Window»35 Основное требование к меню — их стандартизация. Это требо- вание относится ко многим аспектам меню: месту размещения заголовков меню и их разделов, форме самих заголовков, клави- шам быстрого доступа, организации каскадных меню. Цель стан- дартизации — облегчить пользователю работу с приложением. Надо, чтобы пользователю ие приходилось думать, в каком меню и как ему надо открыть или сохранить файл, как ему получить справку, как работать с буфером обмена Clipboard и т.д. Для осу- ществления всех этих операций у пользователя, поработавшего хотя бы с несколькими приложениями Windows, вырабатывает- ся стойкий автоматизм действий и недопустимо этот автоматизм ломать. Начнем рассмотрение требований с размещения заголовков меню. Типичная полоса главного меню приведена на рис. 1.5. Ко- нечно, состав меню зависит от конкретного приложения. Но раз- мещение общепринятых разделов должно быть стандартизиро- ванным. Все пользователи уже привыкли, что меню Фойл разме- щается слева в полосе главного меню, раздел справки — справа, перед ним в приложениях MDI размещается меню Окно и т.д. Главное меню должно также снабжаться инструментальной па- нелью (см. рис. 1.5), быстрые кнопки которой дублируют наибо- лее часто используемые команды меню. На этих кнопках надо использовать, по возможности, привычные картинки. В Delphi имеется множество изображений кнопок на все случаи жизни, но, к сожалению, оня ие всегда имеют привычный для пользова- теля рисунок. Впрочем, в примерах, приложенных к Delphi, можно найти и более привычные изображения для кнопок. По возможности стандартным должно быть и расположение разделов в выпадающих меню. На рис. 1.6 приведены распро- страненные варианты меню Фойл — работа с файлами. Правка — работа с текстами. Окно — управление окнами в приложении MDI и меню Справка или ? — просмотр справочных данных. Об- Рис. 1.5 Типичная полоса главного меню и инструментальная линейка
26 Влсч» 1 Рис. 1.6 " e-ssi О Создать Открыть Закрыть LJ Соуранить F2 Сокранитькак. Предварительный просмотр , ё? Печать Установке принтера... fibres Crt+Q 6) ИДЯ «л Цтмемтть , CW-Z X рьреэать CWrX Колировать CbUC СВ Вставить СИ+V Л Найти. CUhF ?4L Замеьмтъ... ратите внимание, что, например, раздел Выход всегда размещает- ся последним в меню Фойл, а раздел информации о версии про- граммы О программе... — последним в справочном меню. Группы функционально связанных разделов отделяются в вы- падающих меню разде лите лями, как показано на рис. 1.6. Названия разделов меню должны быть привычными пользо- вателю. Если вы не знаете, как назвать какой-то раздел, не изоб- ретайте свое имя, а попытайтесь найти аналогичный раздел в ка- кой-нибудь русифицированной программе Microsoft для Win- dows. Названия должны быть краткими и понятными. Не испо- льзуйте фраз, да и вообще больше двух слов, поскольку это пере- гружает экран и замедляет выбор пользователя. Названия разде- лов должны начинаться с заглавной буквы. Применительно к ятт- глийским названиям разделов существует требование, чтобы каждое слово тоже начиналось с заглавной буквы. Но примени- тельно к русским названиям это правило ие применяется. Названия разделов меню, связанных с вызовом диалоговых окон, должны заканчиваться многоточием, показывающим поль- зователю, что прн выборе этого раздела ему предстоит устано-
Требомни» к интерфейсу полыовсгтсл» приложении для Window»27 вить в диалоге еще какие-то параметры (см. на рис. 1.6 а разделы Сохранить, Сохранить как и др.). Если разрабатывается приложение с интерфейсом множества документов, то меню Окно должно заканчиваться списком от- крытых окон (см. рис. 1.6 в) с указанием активного документа, причем при выборе пользователем в атом списке соответствую- щего окна это окно должно активизироваться. Разделы, к которым относятся каскадные меню (например, раадел Упорядочить на рис. 1.6 в) должны заканчиваться стрел- кой. указывающей на наличие дочернего меню данного раздела. Delphi ставит эту стрелку автоматически, так что о ней вам ду- мать ие приходится. Вообще злоупотреблять каскадными меню ие следует, так как пользователю не так просто до них добирать- ся. Если в дочернем меню должно быть много разделов, напри- мер, свяванных с какими-то опциями и настройками, то поду- майте, не лучше ли вместо этого дочернего меню предусмотреть диалоговое окно, в котором эти опции будут более обозримыми и доступными. В каждом названии раздела должен быть выделен подчерки- ванием символ, соответствующий клавише быстрого доступа к разделу (клавиша Alt плюс подчеркнутый символ). Хотя вряд ли такими клавишами часто пользуются, но традиция указания та- ких клавиш незыблема. В реальной работе, вероятно, они испо- льзуются только в случае, когда отказала мышь. Многим разделам могут быть поставлены в соответствие «го- рячие» клавиши, позволяющие обратиться к команде данного раздела, даже не заходя в меню. Комбинации таких «горячих» клавиш должны быть традиционными. Например, команды вы- резания, копирования и вставки фрагментов текста практически всатда имеют «горячие» клавиши Ctrl-X, Ctrl-C и Orl-V соответст- венно. Заданные сочетания клавиш отображаются в заголовках соответствующих разделов меню (см. рис. 1.6 а и 1.6 б). В Delphi 4 и 5 предусмотрена возможность включать в меню пиктограммы, соответствующие некоторым разделам (см. рнс. 1-6 а и 1.6 6). Это не входит в обязательные требования к меню и использование пиктограмм — дело вкуса. Некоторые разделы меню, соответствующие выбору каких-то нестроен, могут содержать индикаторы, показывающие, выбран или нет данный раздел, и могут содержать радиокнопки. В каче- стве примеров разделов с индикатором можно привести раз- дел меню Поверх остальных, рассмотренный в разделе 1.3 и приве- денный на рис. 1.4, а также список открытых окон на рис. 1-6 в.
Я8 Глава I Не все разделы меню имеют смысл в любой момент работы по- льзователя с приложением. Наириыер, если в приложении ие от- крыт ни один документ, то бессмысленно выполнять команды ре- дактирования в меню Правко. Если в тексте документа ничего ие изменялось, то бессмысленным является раздел этого меню От- менить, отменяющий последнюю команду редактирования. Такие меню и отдельные разделы должны делаться временно недоступ- ными или невидимыми. Это осуществляется заданием значения false свойствам раздела Enabled или Visible соответственно. Раз- личие между недоступными и невидимыми разделами в том, что недоступный раздел виден в меню, но отображается серым цве- том, а невидимый раздел просто исчезает из меню, причем ниже- лежащие разделы смыкаются, занимая его место. Выбор того или иного варианта — дело вкуса и удобства работы пользовате- ля. Вероятно, целиком меню лучше делать невидимыми, а отде- льные разделы — недоступными. Например, пока ни один доку- мент ие открыт, меню Лровка можно сделать невидимым, чтобы, он не отвлекал внимания пользователя. А раздел Отменить этого меню в соответствующих ситуациях лучше делать недоступным, чтобы пользователь видел, что такой раздел в меню есть и им можно будет воспользоваться в случае ошибки редактирования. Вот и все основные требования к главному меню приложения. Надо только добавить, что многие компоненты (например, много- строчные окна редактирования) должны иметь контекстные меню, всплывающие при щелчке на них правой кнопкой мыгпги и обычно дублирующие основные команды главного меню, относя- щиеся к данному компоненту. Кроме того обычно желательно иметь в приложении панель состояния (см. внизу окна на рис. 1.5), в которой даются пользователю некоторые подсказки по работе. 1.6.2 Методика проектирования меню и инструментальной панели Теперь обсудим, как можно удовлетворить рассмотренные в разделе 1.6.1 требования к меню в приложениях Delphi. В Delphi меню создаются компонентами MainMenu — главное меню, и PopupMenu — всплывающее меню. Оба компонента расположе- ны на странице Standard. В рамках данной книга нет возможно- сти подробно рассматривать отдельные операции со всеми компо- нентами (в данном случае — с MainMenu и PopupMenu). Предпо- лагается, что читатель владеет техникой работы с компонентами. Тем, кто чувствует себя в этом не очень уверенно, я бы рекомен- довал приобрести предыдущую книгу из серии «Все о Delphi» — «100 компонентов Delphi 5», в которой подробно рассмотрзны все
Требования и интерфейсу пользователе приложений для Windows 29 компоненты. Так что будем считать, что работу с компонентами вы освоили, и остановимся только на общей методике разработки меню приложения. Ниже рассмотрены основные этапы разработки полноценного меню. В качестве примера ие рис. 1.7 приведена форма приложе- ния, ранее изображенного во время выполнения на рис. 1.5. Це- ликом его код приведен в Приложении в разделе П.1. Рис. 1.7 Форма приложения, изобра- женного ранее на рис. 1.5 Итак, основные этапы разработки сводятся к следующему : 1. Перенесите на форму компонент диспетчеризации действий ActionList (на рис. 1.5 он третий слева в верхнем ряду невизу- альных компонентов). Применение этого компонента ие обя- зательно. Но, привыкнув к работе с ним, вы увидите, что его применение экономит вам много времени и делает код более прозрачным и простым в сопровождении. 2. Перенесите на форму компонент ImageList — список изобра- жений для рааделов меню и быстрых кнопок инструменталь- ной панели (на рис. 1.5 он первый слева в верхнем ряду). 3. С помощью редактора списка изображений, вызываемого двойным щелчком на ImageList, заполните список изображе- ниями, символизирующими основные команды будущего меню (рис. 1.В). Изображение добавляется в список нажатием кнопки Add и последующим выбором файла изображения. С Delphi поставляется много изображений кнопок. Они располо- жены в каталоге \lmoges\Buttons, а сам каталог Images в Delp- hi 4 и 5 расположен в каталоге \program files\common files\bor- land shared. Только учтите, что размер всех изображений в списке должен быть одинаковым. При добавлении в список изображений для кнопок надо иметь в виду, что они часто со- держат ие одно, а два и более изображений. В этих случаях
30 при попытке добавить изображение задается вопрос: «Bitmap dimensions for .. are greater then imagelist dimensions Separate into ... separate bitmaps?» (Размерность изображения ... больше раз- мерности списка. Разделить ие ... отдельных битовых матри- цы?). На заданный вопрос надо отвечать положительно. Тогда загружаемая битовая матрица автоматически разделится на отдельные изображения и потом вы можете удалить те ие них, которые вам не нужны, кнопкой Delete. Рис. 1.8 Редактор списка изоброжений Imogetist 4. Перенесите на форму компоненты стандартных диалогов типа «Открыть файл* (компонент OpenFileDialog), «Сохранить файл как» (компонент SaveFileDialog) и др. На рис. 1.7 диа- логи — все компоненты нижнего ряда. Перенесите также дру- гие компоненты, к которым будут относиться действия. На- пример, на рис. 1-7 на форме расположено многострочное окно редактирования RichEdit. 5. Компонент диспетчеризации действий ActionList свяжите со списком изображений, установив его свойство Images равным ImageListl — имени компонента ImageList. 6. Двойным щелчком на компоненте ActionList вызовите редак- тор действий (рис. 1.9). Вам надо занести в нем все действия, соответствующие будущим разделам меню. Щелчок правой кнопкой мыши или щелчок на маленькой кнопочке со стрел- кой вниз правее первой быстрой кнопки окна редактирования позволит вам выбрать одну из команд: New Action (новое дейст- вие) или New Stondord Action, (новое стандартное действие). Первая на них относится к вводу нового действия любого типа. По умолчанию эти действия будут получать имена Acti- onl, Action2 и т.д. Вторая команда открывает окно, в котором
Требоиония к интерфейсу полыовогеля приложении для Window» 31 вы можете выбрать необходимое вам стандартное действие (или сразу несколько действий). После этого в правом окне (Actions) редактора появятся имена выбранных действий, а в левом (Categories) — категории действий. Рис. 1.Ф Редактор действий компонента AdionLis! IN яте! □ AOwte ЕЫ iSAOpen • QASave ASewAt 7. После занесения в список очередного действия вы должны вы- делить его в правом окне рис. 1.9 и на странице свойств окна Редактора Кода задать для него свойства. Прежде всего задай- те Name (имя) — желательно осмысленное, связанное с вве- денным действием. Только не задавайте имена, которые могут оказаться тождественными именам каких-то функций, мето- дов и т.д. Например, если вы назовете действие Exit, у вас в дальнейшем возникнут проблемы, если в приложении вам надо будет использовать процедуру exit (выход из функции). Чтобы не возникало подобных казусов, можно рекомендовать добавлять ко всем именам в начале символ ♦ А», как это сдела- но на рис. 1.9. Вы должны также установить свойство Caption — надпись, которая далее будет появляться в кнопках, разде- лах меню и т.д. При этом надо следовать всем изложенным в разделе 1.6.1 требованиям: выделение подчеркнутого симво- ла, многоточия в конце разделов, вызывающих диалоговые окна и т.д. Вы можете также задать свойства: Shortcut — бы- стрые клавиши (если они должны назначаться для данного действия). Hint — надписи на ярлычках подсказок и в строке состояний (см. подробнее в разделе 1.9), HelpContext — иден- тификатор темы контекстной справки (если для данного дей- ствия предусмотрена справка). Imageindex — индекс изобра- жения в ImageList, соответствующего данному действию. Мо- жете также, при необходимости, задать свойства Enabled — доступность. Checked — занесение индикатора выделения и ДР-
32 8. Для каждого действия на странице событий Инспектора Объ- ектов определено событие OnExecnte. В обработчике этого со- бытия вам надо описать операции, соответствующие данному действию. Например, для действия AExit (выход из програм мы) обработчик может иметь вид procedure TForml.AExitExecure(Sender: TObject); begin Close; end; Обратите внимание, что заголовок процедуры (AExitExecute) получается осмысленным — он указывает на характер дейст- вия. Такое название процедуры, конечно, много понятнее, чем Button5Click или N1 Click — имена, получающиеся без использования компонента ActionList. 9. После завершения списка действий перенесите на форму ком- понент MainMcnu — главное меню (на рис. 1.7 второй компо- нент слева в верхнем ряду). Установите его свойство Images равным ImageList 1 — имени компонента ImageList. Вызови- те редактор меню (рис. 1.10), сделав двойной щелчок не Main- Menu. В пунктирной рамке задайте заголовок первого выпада- ющего меню, в соседней рамке — второго и т.п. Щелкнув на заголовке одного из меню вы получите доступ к вводу разде- лов выпадающего меню. Выделите очередную пунктирную рамку и в Инспекторе Объектов задайте для раздела свойство Action. Для этого вам достаточно щелкнуть в строке правее этого свойства и из выпадающего списка, в котором имеются все введенные вами действия, выбрать нужное. Вы увидите, что при этом автоматически заполнятся все свойства данного раздела меню: появится надпись, изображение (если опо пре- дусмотрено), тексты подсказок в свойстве Hint и т.д. На стра- нице событий Инспектора Объектов вы увидите, что в событие OnClick (щелчок) уже занесена ссылка на ранее написанный вами для этого действия обработчик. Таким образом, приме- нение компонента ActionList существенно упростило разра- Рис. 1.10 Окно редокгоро меню
Требования к интерфейсу пользователи приложении дли Window»33 ботку меню. Если в каком-то из разделов вам надо сделать ка- скадное меню (см. раздел Выравнивание на рис. 1.10), щелкни- те в окне редактора меню правой кнопкой мыши и во всплыв- шем меню выберите раздел Creole Submenu. 10. Если вы проектируете приложение MDI и вам надо в меню Окно внести список открытых документов (см. рис. 1.6 в), то вам надо обратиться к форме и в ее свойстве WindowMenu из выпадающего списка выбрать раздел, соответствующий заго- ловку выпадающего меню (в нашем примере — это должно быть имя раздела Окно), в конец которого надо заносить спи- сок документов. Сам список будет заноситься в меню автома- тически. 11. Перенесите на форму компонент инструментальной панели ToolBar. Щелкав на нем правой кнопкой мыши выбирайте команды New Button (новая кнопка) или New Separator (новый разделитель). Для каждой новой кнопки так же, как ранее для разделов меню, устанавливайте свойство Action. Все оста- льные свойства перенесутся в кнопку автоматически. 12. Если необходимо обеспечить в приложении контекстные всплывающие меню, то перенесите на форму один или неско- лько (по числу различных контекстных меню) компонентов PopupMenu — на рис. 1.7 это правый компонент в верхнем ряду. Свяжите их с компонентом ImageList, задав свойство Images. Далее, сделав двойной щелчок на PopupMenu, вы по- падете в тот же редактор меню, что и в случае MainMenn. Вы можете проектировать всплывающее меню так же, как глав- ное. Но, поскольку всплывающее меню обычно дублирует ка- кне-то разделы уже сформированного главного меню, то мож- но обойтись копированием соответствующих разделов. Для этого, войдя в конструктор меню из компонента PopupMenu, щелкните правой кнопкой мыши и из всплывшего меню выбе- рите команду Select Menu (выбрать меню). Вам будет предложе- но диалоговое окно, в котором вы можете перейти в главное меню. В нем вы можете выделить нужный вам раздел или раз- делы (при нажатой клавише Shift выделяются разделы в задан- ном диапазоне, при нажатой клавише Ctrl можно выделить со- вокупность разделов, не являющихся соседними). Затем вы- полните копирование их в буфер обмена, нажав клавиши Ctrl-C. После этого опять щелкните правой кнопкой мыши, выберите команду Select Menu и вернитесь в контекстное меню- Укажите курсором место, в которое хотите вставить скопиро- ванные разделы, и нажмите клавиши чтения из буфера обме-
на — Orl-V. Разделы меню вместе со всеми их свойствами бу- дут скопированы в создаваемое вами контекстное меню. 13. Когда контекстное меню сформировано, для привязки его к необходимому компоненту выделите этот компонент на форме и в инспекторе объектов задайте его свойство PopupMenn рав- ным имени компонента соответствующего PopupMenu. Вот в общих чертах последовательность операций при проек- тировании меню и инструментальной панели с использованием компонентов ActionList, ImageList, MainMenu, PopupMenu и TooIBar. Как видите, это не очень быстрый процесс и обидно по- вторять его снова и снова для каждого нового приложения, тем более, что требование стандартизации приводит к тому, что одни и те же разделы с одинаковыми свойствами кочуют из приложе- ния в приложение. Поэтому можно рекомендовать один раз по- тратить время, создать меню, содержащее большинство разде- лов, которые могут вам понадобиться в различных приложени- ях, и затем сохранить его в качестве шаблона. Это можно сделать двумя способами. Во-первых, вы можете сохранить шаблон самого меню. Для этого в редакторе меню выберите из меню, всплывающего при щелчке правой кнопкой мыши, команду Save As Templaie. Эта команда вызывает диалог, представленный на рис. 1.11- В этом диалоге вы можете в верхнем окне указать описание (заголовок), под которым хотите сохранить ваше меню. Впоследствии в лю- бом вашем новом приложении вы можете загрузить этот шаблон в меню, выбирая из всплывающего меню в окне конструктора меню команду Insert From Template. А удалить после этого разделы, не используемые в данном приложении, не представляет никако- Рис. 1.11 Окно сохранения шаблона разработанного ЕсЙ Menu F ie Menu (fa Help Menu Help Menu (Expended) MOI Fieme Menu v/indnw Menu
Требования к интерфейсу пользователя приложений для Windows 35 го труда — вы просто выделяете соответствующий раздел в кон- структоре меню и нажимаете клавишу Del. Второй вариант, более интересный — сохранить в качестве шаблона всю совокупность компонентов — ActionList, Image- List, MainMenn, PopupMenu, ТооТВаг, возможно также и соот- ветствующим образом настроенные компоненты диалогов. Для того, чтобы сделать это, надо выделить на форме требуемые ком- поненты и выполнить команду Component | Create Component Temp- late (создать шаблон компонента). Перед вами откроется диалого- вое окно Component Template Infomation (рис. 1.12). В его верхнем окошке редактирования Component Name вы можете задать имя шаблона. Это имя будет появляться на ярлычке подсказки, если пользователь задержит курсор мыши над пиктограммой вашего шаблона в палитре библиотеки. На рис. 1.12 это имя —- ТМуМе- пп. В выпадающем списке в средней части окна вы можете вы- брать страницу библиотеки визуальных компонентов, на которой хотите разместить пиктограмму шаблона. Вы можете также ука- зать новое имя (Мои шаблоны на рис. 1.12) и тогда в библиотеке визуальных компонентов будет создана новая страница с этим именем. Можно также изменить пиктограмму данного компо- нента (кнопиа Change). По умолчению вам предлагается пиктог- рамма компонента, который был вами выделен первым при вы- делении группы. Если вы хотите сменить пиктограмму, она дол- жна быть подготовлена заранее в виде файла .bmp размером 24 на 24. После выполнения всех описанных операций щелкните на кнопке ОК. Рис. 1.12 Диалоговое окно задания ин- формации о шаблоне В результате этих действий ваш шаблон появится в библиоте- ке. Вы можете убедиться в этом, посмотрев на указанную вами страницу библиотеки. При последующем переносе его в новое приложение вместе с ним перенесутся все свойства компонентов и даже обработчики событий. Вам останется только убрать то лишнее, что вам не нужно, и добавить что-то новое.
36 Гиово 1 Не жалейте времени на тщательную проработку фрагментов, которые мо- гут повторяться в разных приложениях. Но, чтобы ваша робота не пропада- ло дором, сохраняйте шаблоны этих фрагментов но страницах библиотеки. Со временем у вех накопится хорошая библиотека собственных фрагмен- тов и ее использование в последующих проектах позволит вам проектиро- вать быстро и качественно, сохраняя при этом стилистическое единство ва- ших разработок. 1.7 Компоновка форм, применение фреймов, создание иерархии форм и фреймов Каждое окно, которое вы вводите в свое приложение, должно быть тщательно продумано и скомпоновано. Удачная компонов- ка может стимулировать аффективную работу пользователя, а неудачная — рассеивать внимание, отвлекать, заставлять тра- тить лишнее время на поиск нужной кнопки или индикатора. Управляющие элементы и функционально связанные с ними компоненты экрана должны быть зрительно объединены в груп- пы, заголовки которых коротко и четко поясняют их назначение. Такое объединение позволяют осуществлять различные панели. Можно рекомендовать, как правило, раамещать компоненты не непосредственно на форме, а па панелях. Но и внутри панелей надо продумывать размещение компонентов как с точки зрения эстетики, так и с точки зрения визуального отражения взаимоот- ношений элементов. Например, если имеется кнопка, которая разворачивает окно списка, то эти даа компонента должны быть визуально связаны между собой: размещены на одной панели и в непосредственной близости друг от друга. Если же ваш экран представляет собой случайные скопления кнопок, то именно так он и будет восприниматься. И в следующий раз пользователь не захочет пользоваться вашей программой. Каждое окно должно иметь некоторую центральную тему, ко- торой подчиняется его композиция. Пользователь должен пони- мать, для чего предназначено данное окно и что в нем наиболее важно. При этом недопустимо перегружать окно большим чис- лом органов управления, ввода и отображения информации. В окне должно отображаться главное, а все детали и дополнитель- ную информацию можно отнести на вспомогательные окна. Для этого полезно вводить в окно кнопки с надписью Больше..., много- точие в которой показывает, что при нажатии этой кнопки от- кроется вспомогательное окно с дополнительной информацией.
Требовании к интерфейсу пользователи приложении для Window;37 Помогают также разгрузить окно многостраничные компоненты с закладками. Они дают возможность пользователю легко пере- ключаться между разными по тематике страницами, на каждой из которых имеется необходимый минимум информации. При- меры удачной организации окон вы можете посмотреть в Delphi, выполнив команду Tools | Environment Options и полистав страницы окна опций. Еще один принцип, которого надо придерживаться при проек- тировании окон — стилистическое единство всех окон в прило- жении. Недопустимо, чтобы сходные по функциям органы управления в разных окнах назывались по-разному или разме- щались в разных местах окоп. Все это мешает работе с приложе- нием, отвлекает пользователя, заставляет его думать не о сущно- сти работы, а о том, как приспособиться к тому или иному окну. Стилистическому единству окон приложения способствует имеющаяся в Delphi возможность создания иерархии форм. Пусть вы, например, решили, что формы вашего приложения в основном будут строиться так, как показано на рис. 1.13: иметь две панели с перемещаемыми границами, в которых будут разме- щаться окна редактирования и списки, ниже них — панель, в ко- торой будут располагаться кнопки управления, а внизу окна дол- жна быть панель состояния. На форме должен также размещать- ся невизуальный компонент ApplicationEvents, в обработчике события OnHint которого записан оператор StatusBarl. SunpleText: Application .Hint; обеспечивающий отображение подсказок в панели состояния (по- дробнее об этом см в разделе 1.9). Рис. 1.13 Спроектировав такую форму с перестраиваемыми панелями (подробнее об этом см. в разделе 2.3, а о панели состояния с под- сказками — в разделе 1-9), сохраните ее модуль в файле. Далее вы можете включить эту форму в Депозитарий — хранилище форм и проектов. Для этого щелкните на вашей форме правой кнопкой мыши и выберите во всплывшем контекстном меню раз-
за дел Add То Repository. Откроется диалоговое окно, вид которого приведен на рис. 1.14. В верхнем окне Title вы должны написвть название вашей формы — подпись под ее пиктограммой при вхо- де в Депозитарий. В следующем окне — Description можете напи- сать более развернутое пояснение. Его может увидеть пользова- тель, войдя в Депозитарий, щелкнув правой кнопкой мыши и выбрав во всплывшем меню форму отображения View Details. В выпадающем списке Раде вы можете выбрать страницу Депози- тария, на которой хотите разместить пиктограмму своей формы. Впрочем, вы можете указать и новую страницу с новым заголов- ком. В результате она появится в Депозитарии. Рис. 1.14 Окно добоаления формы или фрейма в Депозитарий В окне Author вы можете указать сведения о себе как об авторе. Наконец, если стандартная пиктограмма вас не устраивает, вы можете выбрать другую, щелкнув на кнопке Browse . После вы- полнения всех этих процедур щелкните на кнопке ОК и ваша форма окажется включенной в Депозитарий. Теперь вы можете использовать ее в последующих ваших при- ложениях. Для этого вам надо будет выполнить команду File | New и в открывшемся диалоговом окне New Items отыскать вашу фор- му (рис. 1.15). Рис. 1.15 Окно New Items с включенной новой формой
В нижней части окна расположены три радиокнопки, которые определяют, как именно вы хотите заимствовать форму из Депо- зитария: Сору — копировать, Inherit — наследовать. Use — испо- льзовать. Если включена кнопка Сору, то файлы формы просто будут скопированы в ваше приложение. При этом никакой даль- нейшей связи между исходной формой и копией не будет. Вы мо- жете спокойно изменять свойства вашей копии и это никак не от- разится на форме, хранящейся в Депозитарии. А если вы в даль- нейшем что-то измените в форме, хранящейся в Депозитарии, то эти изменения никак не затронут вашего приложения, куда вы до этого скопировали форму. При включенной кнопке Inherit вы получите в своем проекте форму, наследующую размещенной в Депозитарии. Это значит, что если вы что-то измените в форме, хранящейся в Депозита- рии, то это отразится при перекомпиляции во всех проектах, ко- торые наследуют эту форму. Однако, изменения в наследуемых формах никак не скажутся на свойствах формы, хранящейся в Депозитарии. При включенной кнопке Use вы получите режим использова- ния. В этом случае в ваш проект включится сама форма, храня- щаяся в Депозитарии. Значит любое изменение свойств формы, сделанное в вашем проекте, отразится и на хранящейся в Депо- зитарии форме, и на всех проектах, наследующих или использу- ющих эту форму (при их перекомпиляции). Таким образом, режим Inherit целесообразно использовать для всех модулей вашего проекта, я режим Use — для изменения ба- зовой формы. Тогда усовершенствование вамп базовой формы бу- дет синхронно сказываться на всех модулях проекта при нх пере- компиляции. Мы рассмотрели включение в Депозитарий одной формы. Но далее на ее основе вы можете создать следующий уровень иерар- хии — например, внести на нижнюю панель стандартные кнопки управления и снова сохранить форму в Депозитарии под новым именем. Таким образом и создается иерархия форм проекта В Delphi 5 введен еще один компонент, который помогает под- держивать стилистическое единство приложения. Это Frame — фрейм. Он представляет собой нечто среднее между панелью и формой. С формой его роднит то, что он: проектируется отдельно, как самостоятельное окно имеет свой модуль — файл .pas имеет возможности наследования, причем даже более широ- кие, чем у формы, так как может наследоваться даже внутри одного приложения
может включаться в Депозитарий и использоваться так же, как и форма, включая наследование С панелью фрейм роднит то, что он: не является самостоятельным окном Windows и может отобра- жаться только на форме или другом контейнере имеет свойства, методы, события, подобные панели, а не форме Таким образом, фрейм — это панель, т.е. некий фрагмент окна приложения, но способный переносится на разные формы, в разные приложения и допускающий использование преиму- ществ наследования. Начать проектирование нового фрейма можно командой File | New Frame или командой File | New и выбором пиктограммы Frame на странице New окна Депозитария. В обоих случаях перед вами откроется окно фрейма, подобное окну формы, а в Редакторе Кода вы увидите текст заготовки модуля фрейма, подобный зего- товке модуля формы, только класс фрейма наследует TFrame, а не TForm, как класс формы. На фрейм вы можете так же, как на форму, переносить и раз- мещать любые компоненты, устанавливать их свойства, писать обработчики нх событий и т.п. Рассмотрим пример фрейма. Во многих диалогах при установ - ке различных опций фигурирует фрагмент, фрейм которого по- казан на рис. 1.16. Фрагмент включает в себя панель Group Box, окно редактирования, в котором пользователь может написать имя файла, кнопку Обзор, которая позволяет выбрать файл в стандартном диалоге Windows открытия файла. Если путь к фай- лу длинный, то полное имя файла с путем может не помещаться в окне редактирования. Поэтому полезно для него предусмотреть всплывающее окно, которое отображало бы полное имя файла вместе с путем и всплывало бы, если пользователь задержал над ним курсор мыши. Девайте построим подобный фрейм и опробуем его в работе. Начните новое приложение и выполните команду File | New Frame- Перенесите на фрейм групповую панель GroupBox. Перенесите в Рис. 1.16 Фрейм выборе фойло
Требодония к интерфейсу польаоаотели приложений дли Windows 41 эту панель окно редактирования Edit, кнопку Button, диалог OpenDialog и компонент ApplicationEvents — перехватчик со- бытий приложения. Расположите компоненты примерно так. как показано на рис. 1.16. Тем, кто не знаком с этими компонен тами и, прежде всего, с новым компонентом ApplicationEvents придется изучить их по встроенной справке Delphi или ознак> миться по предыдущей книге серии «Все о Delphi* — «100 ко г понентов общего назначения Delphi 5», так как рассмотрение oi дельных компонентов выходит за рамки данной книги. Поэтом будем считать, что представление об этих компонентах у вас имг ется, и ограничимся написанием соответствующих кодов и зада- нием нужных свойств без особо развернутых комментариев. Задайте в свойстве Filter диалога OpenDialog какой-то фильтр файлов, например, «все фойлы|**». Свойство ShowHint (показать ярлычок подсказки) в компонентах Edit и Button установите в true. В кнопке Button кроме того можете написать текст под- сказки Hint, например, «Выбор файла|Выбор файла из каталога». В обработчик события OnShowHint компонента Application- Events занесите оператор: if Hintinfo.HintControl = Editl then begin HintStr:=Editl.Text; ApplicationEven tsl.CancelDispatch; end; Этот оператор в момент, когда должен отображаться ярлычок, проверяет, не является ли источником этого события (HinlTn- fo HintControl) окно редактирования Editl. Если да, то текст яр- лычка (HintStr) подменяется текстом, содержащимся в окне ре- дактирования, и принимаются меры (метод CancelDispatch), чтобы это событие не обрабатывалось д ругими компонентами Ap- plicationEvents, которые могут присутствовать в приложении. Пояснение всех этих операций, связанных с отображением яр- лычков, см. в разделе 1.9. Теперь введите в модуль фрейма глобальную переменную File- Name типа string, в которой будет отображаться выбранный файл, В обработчик щелчка на кнопке введите оператор if OpenDialogl.Execute then begin Editl.Text := OpenDialogl.FileName; FileName := OpenDialogl.FileName; end; который вызывает диалог открытия файла и помещает в скно ре- дактирования Editl и в переменную FileName имя файла, вы- бранного пользователем, вместе с путем к нему.
42 В обработчик события OnExit компонента Editl поместите оператор FileName:=Editl.Text; заносящий в переменную FileName имя файла, если пользователь не пользовался диалогом, а просто написал в окне имя файла. Программирование фрейма закончено. Теперь давайте созда дим тестовую программу, использующую этот фрейм. Предполо- жим, что вам нужно разместить на форме два фрагмента, описан них вами во фрейме (рис. 1.17). Перейдите в основной модуль ва- шего приложения, выберите на странице Standard библиотеки компонент Frame И щелкните на форме. Появится диалоговое окно, в котором будет спрашиваться, какой фрейм вы хотите раз- местить на форме. Выберите ваш фрейм Frame2 и он появится на форме. Можете отбуксировать его, как обычный компонент, в нужное место. Повторите эту операцию еще раз и разместите на форме второй фрейм. Как видите, на форму дважды перенеслись все компоненты фрейма. Но если вы посмотрите текст модуля приложения, то увидите, что в него добавились только два ком- понента фрейма. Все компоненты, размещенные на фрейме — это локальные элементы фреймов, так что никакой путаницы в име- нах компонентов возникнуть не может. Рис. 1.17 Приложение с двумя фреймами, его форма (о) и приложение во время выполнения (б)
Трабоесжия к интерфейсу пользователя приложений дли Windows 43 Теперь вы можете поменять что-то в размещенных на форме объектах фреймов. Например, изменить надписи групповых па- нелей (рис. 1.17 6). Вы увидите, что отдельные фреймы на форме независимы друг от друга. В то же время они наследуют от разря ботанного вами фрейма те свойства, которые не были изменены объектах. Вы можете легко это проверить, вернувшись в окно в. шего фрейма и изменив, например, стиль шрифта групповой п. нели, сделав его жирным. Вы увидите, что шрифт станет жи| ным в обоих фреймах формы. Может сохранить ваше приложение вместе с модулем фрейма оттранслировать его и проверить в работе (рис. 1.17 б). Если вы намерены использовать разработанный вами фрейм в различных своих приложениях, вы можете, щелкнув на нем правой кнопкой мыши, выбрать из всплывшего меню раздел Add То Repository, который позволит сохранить фрейм в Депозитарии точно так же, как вы делали это ранее для формы (см. рис. 1.14. и 1.15). В дальнейшем вы можете использовать фрейм из Депозитария в тех же трех режимах, которые были описаны ранее для форм. Таким образом. Депозитарий позволяет обеспе- чить единство стилистических решений не только внутри прило- жения, но и в рамках серии разрабатываемых вами приложений. Вам достаточно один раз разработать какие-то часто применяе- мые формы и фреймы — ввода пароля, запроса или предупреж- дения пользователя, комбинаций управляющих элементов и т.п., включить их в депозитарии, а затем вы можете использо- вать их многократно во всех своих проектах. Завершая разговор о стиле приложений, надо отметить, что вы должны заботиться не только о единстве стиля внутри своих приложений, но и о том, как ваши приложения впишутся в об- щую организацию рабочего пространства системы и как они бу- дет взаимодействовать с другими приложениями. Если приложе- ние выпадает из общего ансамбля, оно напрашивается на непри- ятности. 1.8 Последовательность фокусировки элементов При проектировании приложения важно правильно опреде- лить последовательность табуляции оконных компонентов. Под этим понимается последовательность, в которой переключается фокус с компонента на компонент, когда пользователь нажимает клавишу табуляции Tab. Это важно, поскольку в ряде случаев по- льзователю удобнее работать не с мышью, а с клавиатурой. Пусть, например, вводя данные о каком-то сотруднике пользова-
44 тель должен в отдельных окнах редактирования указать фами- лию, имя и отчество. Конечно, набрав фамилию, ему удобнее на- жать клавишу Tab и набирать имя, а потом опять, нажав Tab, на- бирать отчество, чем каждый раз отрываться от клавиатуры, хватать мышь и переключаться в новое окно редактирования. Свойство формы ActiveControl, установленное в процессе про- ектирования, определяет, какой из размещенных на форме ком- понентов будет в фокусе в первый момент при выполнении при- ложения. В процессе выполнения это свойство изменяется и по- казывает тот компонент, который в данный момент находится в фокусе. Последовательность табуляции задается свойствами TabOrder компонентов. Первоначальная последовательность табуляции определяется просто той последовательностью, в которой разме- щались управляющие элементы на форме. Первому элементу присваивается значение TabOrder, равное О, второму 1 и т.д. Значение TabOrder, равное нулю, означает, что при первом появ- лении формы на экране в фокусе будет именно этот компонент (если не задано свойство формы ActiveControl). Если на форме имеются панели, фреймы и иные контейнеры, включающие в себя другие компоненты, то последовательность табуляции со- ставляется независимо для каждого компонента-контейнера. На- пример, для формы будет последовательность, включающая не- которую панель как один компонент. А уже внутри этой панели будет своя последовательность табуляции, определяющая после- довательность получения фокуса ее дочерними оконными компо- нентами. Каждый управляющий элемент имеет уникальный номер Tab- Order внутри своего родительского компонента. Поэтому измене- ние значения TabOrder какого-то элемента на значение, уже су- ществующее у другого элемента, приведет к тому, что значения TabOrder всех последующих элементов автоматически изменят- ся, чтобы не допустить дублирования. Если задать элементу зна- чение TabOrder, большее, чем число элементов в родительском компоненте, он просто станет последним в последовательности табуляцни. Из-за того, что при изменении TabOrder одного компонента могут меняться TabOrder других компонентов, устанавливать эти свойства поочередно в отдельных компонентах трудно. Поэ- тому в среде проектирования Delphi 5 имеется специальная команда Edit | Tab Order, позволяющая в режиме диалога задать последовательность табуляции всех элементов.
Требования к интерфейсу пользователя приложений для Window» 45 Значение свойства TabOrder играет роль, только если другое свойство компонента — Tabstop установлено в true и если ком- понент имеет родителя. Например, для формы свойство TabOr- der имеет смысл только в случае, если для формы задан родител» в виде другой формы. Установка TabStop в false приводит тому, что компонент выпадает не последовательности табуляцг и ему невозможно передать фокус клавишей Tab (однако предо фокус мышью, конечно, можно). Имеется и программная возможность переключения фокх са — это метод SetFocus. Например, если вы хотите переключить в какой-то момент фокус на окно Edit2, вы можете сделать это оператором: Edit2.SetFocus; Выше говорилось, что в приложении с несколькими окнами редактирования, в которые польвователь должен поочередно вво- дить какие-то данные, ему удобно переключаться между окнами клавишей табуляции. Но еще удобнее пользователю, закончив- шему ввод в одном окне, нажать клавишу Enter и автоматически перейти к другому окну. Это можно сделать, обрабатывая собы- тие нажатия клавиши OnKeyDown (см. о событиях при нажатии клавиш в разделе 3.2). Например, если после ввода данных в окно Editl пользователю надо переключаться в окно Edit2, то об- работчик события OnKeyDown окна Editl можно сделать следу- ющим: procedure TForml.EditlKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); begin if (Key = VKJRETURN) then Edit2.SetFocus; end; Единственный оператор этого обработчика проверяет, не яв- ляется ли клавиша, нажатая пользователем, клавишей Enter. Если это клавиша Enter, то фокус передается окну Edit2 методом SetFocus. Можно подобные операторы ввести во все окна, обеспечивая требуемую последовательность действий пользователя (впрочем, свобода действий пользователя от этого не ограничивается, по- скольку он всегда может вернуться вручную к нужному окну). Однако можно сделать все это более компактно, используя один обработчик для различных окон редактирования и кнопок. Для этого может использоваться метод FindNextControl, возвращаю- щий дочерний компонент, следующий в последовательности та- буляции. Если компонент, имеющий в данный момент фокус, яв- ляется последним в последовательности табуляции, то возвраща-
46 ется первый компонент этой последовательности. Метод опреде- лен следующим образом: function FindNextControl(CurControl: TWinControi; GoForward, CheckTabStop, CheckParent: Boolean) : TWinControi; Он находит и возвращает следующий за указанным в парамет- ре CurControl дочерний оконный компонент в соответствии с по- следовательностью табуляции. Параметр GoForward определяет направление поиска. Если он равен true, то поиск проводится вперед и возвращается компо- нент, следующий за CurControl. Если же параметр GoForward равен false, то возвращается предшествующий компонент. Параметры CheckTabStop и CheckParent определяют условия поиска. Если CheckTabStop равен true, то просматриваются то- лько компоненты, в которых свойство Tabstop установлено в true. При CheckTabStop равном false значение TabStop не при- нимается во внимание. Если параметр CheckParent равен true, то просматриваются только компоненты, в свойстве Parent кото- рых указан данный оконный элемент, т.е. просматриваются то- лько прямые потомки. Если CheckParent равен false, то про- сматриваются все, даже косвенные потомки данного элемента. Таким образом, для решения поставленной нами задачи — ав- томатической передачи фокуса при нажатии клавиши Enter, вы можете написать единый обработчик событий OnKeyDown всех интересующих вас оконных компонентов, содержащий оператор: if (Key = VK_RETURN) then FindNextControl(Sender as WinControl, true, true, false).SetFocus; Этот оператор обеспечивает передачу фокуса очередному ком- поненту. Для кнопок аналогичный оператор можно вставить в обработчик события OnClick: FindNextControl(Sender as TWinControi, true, true, false).SetFocus; 1.9 Подсказки и контекстно-зависимые справки Приложение должно предельно облегчать работу пользовате- ля, снабжая его системой подсказок, помогающих сориентирова- ться в приложении. Эта система включает в себя: Ярлычки, которые всплывают, когда пользователь задержит курсор мыши над каким-то элементом окна приложения. В частности, такими ярлычками обязательно должны снабжать- ся быстрые кнопки инструментальных панелей, поскольку
Тробо»анм« к интерфейсу полыовдтеля прмяожете<и для Windows 47 нанесенные на них пиктограммы часто не настолько вырази- тельны, чтобы пользователь без дополнительной подсказки мог понять их назначение. Более развернутые подсказки в панели состояния или в дру гом отведенном под это месте экрана, которые появляются при перемещении курсора мыши в ту или иную область окн. приложения. Встроенную систему контекстио-зависимой оперативное справки, вызываемую по клавише F1. Раздел меню Справка, позволяющий пользователю открыть стандартный файл справки Windows -hip, содержащий в виде гипертекста развернутую информацию по интересующим по- льзователя вопросам. Тексты ярлычков и подсказок панели состояния устанавлива- ются для любых визуальных компонентов в свойстве Hint в виде строки текста, состоящей из двух частей, разделенных символом вертикальной черты ’|‘. Первая часть, обычно очень краткая, предназначена для отображения в ярлычке; вторая более развер- нутая подсказка предназначена для отображения в панели состо- яния или ином заданном месте экрана. Например, в кнопке, со- ответствующей команде открытия файла, в свойстве Hint может быть задан текст: Открыть 1Открытие текстового файла Как частный случай, в свойстве Hint может быть задана толь- ко первая часть подскааки без символа Для того, чтобы первая часть подсказки появлялась во всплы- вающем ярлычке, когда пользователь задержит курсор мыши над данным компонентом, надо сделать следующее: 1. Указать тексты свойства Hint для всех компонентов, для ко- торых вы хотите обеспечить ярлычок подсказки. 2. Установить свойства ShowHint (показать подсказку) этих ком- понентов в true или установить в true свойство ParentShowHint (отобразить свойсто ShowHint родителя) и установить в true свойство ShowHint контейнера, содержащего данные компо- ненты. Конечно, вы можете устанавливать свойства в true или false программно, включая и отключая подскааки в различных режи- мах работы приложения. При ShowHint, установленном в true, окно подсказки будет всплывать, даже если компонент в денный момент недоступен (свойство Enabled ~ false).
48 Глава 1 Если вы не задали значение свойства компонента Hint, но установили в true свойство ShowHint или установили в true свойство ParentShowHint, а в родительском компоненте Show- Hint - true, то в окне подсказки будет отображаться текст Hint из родительского компонента. Правда, все описанное выше справедливо при значении свой- ства ShowHint приложения Application равном true (это значе- ние задано по умолчанию). Если установить Application.Show- Hint в false, то окна подсказки не будут появляться независимо от значений ShowHint в любых компонентах. Свойства Hint компонентов можно также использовать для отображения текстов заключенных в них сообщений в какой-то метке или панели с помощью функций GetShortHint и GetLong- Hint, первая из которых возвращает первую часть сообщения, а вторая — вторую (если второй части нет, то возвращается первая часть). Например, эти функции можно использовать в обработчи- ках событий OnMouseMove, соответствующих прохождению курсора мыши над данным компонентом. Так обработчик procedure TForml.LabellMouseMove(Sender: TGbject; Shift: TShiftState; X, Y: Integer}; begin Panel1.Caption := GetShortHint( (Sender as TCcntrol).Hint); Panel2.Caption := GetLongHint( (Sender as TContrcl).Hint); end; отобразит в панели Panell первую, а в панели Рапе12 — вторую часть свойстаа Hint всех компонентов, над которыми будет пере- мещаться курсор, если в этих компонентах в событии OnMouse- Move указан этот обработчик LabellMouseMove. Причем это не зависит от значения их свойстаа ShowHint. Еще один пример. Пусть вы хотите, чтобы при нажатии неко- торой кнопки Buttonl вашего приложения в панели Panell вы- свечивалась подсказка пользователю, например, «Укожите имя файло», а сама кнопка имела всплывающий ярлычок подскааки с текстом «Ввод». Тогда вы можете задать свойстау Hint этой кноп- ки значение «Ввод]Укажите имя файла», задать значение true свой- ству ShowHint, а в обработчик события нажатия этой кнопки вставить оператор Panell.Caption := jGetLongHint(Buttonl.Hint) ; Если же вы не хотите отображать ярлычок подскааки для кнопки, то можете ограничиться значением Hint, равным 'Укожи- те имя файло', а приведенный выше оператор оставить неизмен-
Трюбошания к интерфейсу польаователп приложений для Window»49 ньгм или заменить на эквивалентный ему в данном случае опера- тор Panel1.Caption := GetShortHint(Buttonl.Hint): или даже просто на оператор Panel1.Caption := Butconi.Hint; Перед тем моментом, когда должен отобразиться ярлычок ка- кого-нибудь компонента, возникает событие приложения OnShow- Hint. В обработчике этого события можно организовать какие-то дополнительные действия, например, изменить отображаемый текст. Особенно легко работать с событиями приложения в Delp- hi 5, в которой имеется компонент ApplicationEvents, перехва- тывающий все эти события. В обработчик его события OnShow- Hint можно поместить те операторы, которые надо выполнить перед отображением ярлычка. Заголовок этого обработчика име- ет вид: procedure ApplicationEventsIShowHint( var HintStr: String; var CanShow: Boolean: var Hintinfo: THintlnfo); Здесь передаваемый по ссылке параметр HintStr — отобража- емый в ярлычке текст. В обработчике этот текст можно изме- нить. Так же по ссылке передается параметр CanShow. Если в об- работчике установить его равным false, то ярлычок отображать- ся не будет. Третий параметр, передаваемый по ссылке — Hint- Info. Это структура, поля которой содержат информацию о яр- лычке: его координаты, цвет, задержки появления и т.п. В част- ности, имеется поле HintControl — компонент, сообщение кото- рого должно отображаться в ярлычке, и поле HintStr — отобра- жаемое сообщение. По умолчанию Hintlnfo.HintStr — первая часть свойства Hint компонента. Но в обработчике это значение можно изменить. Рассмотрим пример. Часто текст, записанный в окнах редак- тирования, не виден целиком, так как длина текста превышает размер окна. Полезно предусмотреть, чтобы при перемещении пользователем курсора мыши в окно редактирования во всплы- вающем окне ярлычка отображался полный текст, содержащий- ся в окне. Аналогичные процедуры неплохо предусмотреть в спи- сках, которые также могут содержать текст, не помещающийся целиком в окне. Давайте посмотрим, как это можно сделать. Начните новое приложение и поместите на форму несколько окон редактирова- ния Edit. Установите в них свойстао ShowHint в true. Поместите на форму также компонент ApplicationEvents со страницы Additi- onal и в обработчике его события OnShowHint напишите текст:
50 if (Hintinfo.Hintcontrol.ClassHante - ’TEdit’) then begin HintStr:=(HintInfo-HintControl as TEdit).Text; ApplicationEventsl-CancelDispatch; end; Структура if проверяет, является ли компонент (Hintlnfo.Hint- Control), ярлычок которого должен отображаться, окном редакти- рования типа TEdit. Если является, то текст ярлычка (HintStr) подменяется текстом окна редактирования. Для этого с помощью операции аз компонент Hintlnfo.HintControl рассматривается как объект типа TEdit. Последний оператор Cance IDispatch вве- ден для того, чтобы, если в приложении имеются еще компоненты ApplicationEvents, то они не обрабатывали бы это событие. Аналогичный пример уже рассматривался ранее в разделе 1.7. Имеется еще один способ отображения второй части сообще- ния, записанного в Hint, в строке состояния ияи какой-то облас- ти экрана в моменты» когда курсор мыши проходит над компо- нентом — это использование обработки события приложения On- Hint. Это событие не того компонента, над которым проходит курсор мыши, а именно приложения — объекта Application. В Delphi б это событие также перехватывается компонентом Appli- cations vents. Если обработчик этого события определен, то в мо- мент прохождения курсора над компонентом, в котором задано свойство Hint, вторая часть сообщения компонента заносится в свойство Hint объекта Application. Если Свойство Hint компо- нента содержит только одну часть, то в свойство Hint объекта Application заносится эта первая часть. Если ваше приложение содержит инструментальную панель с быстрыми кнопками, то, как правило, эти кнопки должны снаб- жаться не только всплывающими ярлычками, но и развернуты- ми подсказками в панели состояния (см. пример панели на рис. 1.5). Если у вас есть подобный пример, вы можете опробовать на нем методику отображения подсказок в панели состояния. Но проще просто продолжить предыдущий пример. Поместите на форму панель состояния — компонент StatusBar со страницы Win32 и установите его свойство SimplePanel в true. Тем самым вы превратили панель в односекционную. Если вы хотите испо- льзовать миогосекциоияую панель, то в приведенном ниже опе- раторе надо заменить свойство SimplcText ссылкой на панель, в которой вы хотите отображать подсказку. Напишите какие-нибудь тексты в свойствах Hint ваших окон редактирования. А в обработчик события On H int компонента ApplicationEvents вставьте оператор StatusBarl .SiojpleText := Application.Hint;
Требования к интерфейсу пользователя приложении для Windows 31 Запустите приложение на выполнение. Вы увидите, что тек- сты подсказок отображаются в панели состояния, когда курсор мыши перемещается над тем или иным окном редактирования. Причем это не мешает появляться ярлычкам, отображающим тексты окон. В версиях Delphi, предшествующих Delphi 5, обработка события OnHint приложения делается несколько сложнее. Вам надо ввести функцию обработки события OnHint (назовем ее Dis- playHint), которая может содержать тот же оператор, который приводился ранее: Statu sBar1.S impleText := Application.Hint; обеспечивающий отображение текста, переданного компонентом в Application.Hint. Прототип этой функции можно внести в описание класса фор- мы. Кроме того надо указать приложению на эту функцию как ня обработчик события OnHint. Это можно сделать, например, задав в обработчике события OnCreate формы оператор: Application.OnHint := DisplayHint; В итоге текст вашего модуля может иметь вид: type TForml = class (TForm) StatusBarl: TStatusBar; SBOpen: TSpeedButton; SBClose: TSpeedButton; SBSave: TSpeedButton: procedure FormCreate(Sender: TObject); private { Private declarations ) procedure DisplayHint(Sender: TObject); public { Public declarations ) var Forml: TForml; implementation {$R *.DFM) procedure TForml.DisplayHint(Sender: TObject); begin StatusBarl.SimpleText := Application.Hint; end;
52 Глава 1 procedure TForml.Formereate(Sender: TObject); Application.OnHint := DisplayHint; end; end. Как видим, в ранних версиях Delphi все это получается более грамоздко, чем в Delphi 5. Более подробные пояснения пользователю может дать контек- стно-зависимая справка, встроенная в приложение. Она позволя- ет пользователю нажать в любой момент клавишу F1 и получить развернутую информацию о том компоненте, который в данный момент находится в фокусе. Для того, чтобы ато осуществить, надо разработать для своего приложения файл справки .Ыр. За- тем надо в компонентах, для которых вы хотите обеспечить кон- текстно-зависимую справку, установить в свойстве HelpContext номер контекстной справки, соответствующей данному компо- ненту. Если HelpContext компонента равен нулю, то данный компонент наследует это свойство от своего родительского ком- понента. Например, для всех компонентов, размещенных ня не- которой панели, можно задать HelpContext = О, а для самой па- нели задать отличное от нуля значение HelpContext, соответст- вующее теме, описывающей назначение всех компонентов пане- ли. Для того, чтобы все это работало, надо выполнить команду Project | Options и в окне Project Options (опции проекта) на страни- це Application (см. рис. 1.3) установить значение опции Help file, равное имени подготовленного файла .Ыр. Это приведет к тому, что в головном файле проекта появится оператор вида: Applicat ion.HelpFile := ’<имя файла .h1р>'; В этом операторе используется метод HelpFile, определяющий файл справки, к которому обращается проект. Этот метод и по- добный оператор можно использовать в приложении в любом об- работчике события, если в какие-то моменты требуется сменить используемый файл справки. Если предполагается, что файл справки будет расположен в том же каталоге, где находится само приложение, то имя файла и в окне Опции проекта и в приведенном выше операторе надо задавать без указания пути. Иначе приложение, работающее на вашем компьютере, перестанет работать на компьютере пользователя, у которого каталоги не совпадают с вашими. Для того, чтобы приложение в свойствах HelpContext могло ссылаться на какой-то номер контекстной справки, в файле проек-
Требования к интерфейсу пользователя приложений длм Window»53 та справки .hpj в разделе [МАР] надо поместить таблицу соответ- ствия использованных значений HelpContext и тем файле .Ыр. В заключение поговорим о традиционном разделе меню Справ- ка, позволяют ам пользователю открыть файл справки .Ыр н по- лучить развернутую информацию по всем вопросам, связанным с данным приложением. В обработчик события при выборе данно- го раздела меню или при нажатии соответствующих кнопок по- мощи вставляются операторы вида Application.HelpContext(<номер темы>) Задаваемые в этих операторах номера тем аналогичны исполь- зуемым при задании свойств HelpContext. Это номер той темы, которая первой отобразится при открытии окна справки. А в да- льнейшем пользователь, как обычно, может перейти, работая с программой справки, к любой интересующей его теме. Имеется еще несколько методов объекта Application, обеспе- чивающих работу со справочными файлами. Метод Helpjump: function HelpJump(const JumpID: string): Boolean; выполняет действия, аналогичные HelpContext, но его параметр JumpID — не идентификатор темы, а имя соответствующей темы в файле справки, задаваемое в нем сноской Метод HelpCommand: function HelpCommand(Command: Word; Data: Longint): Boolean; позволяет выполнить указанную параметром Command команду API WinHelp с параметром Data. Метод генерирует событие On- Help активной формы или приложения, а затем выполняет ука- занную команду WinHelp. Полный список команд WinHelp вы можете найти в теме WinHelp справочного файла win32.hlp, рас- положенного в каталоге ...\Progrom Files\Common Files\Borlond Sho- red\MSHelp. Приведен только некоторые из них. Команда HELP_CONTENTS с параметром О отображает окно Содержание справки. Команда HELP_INDEX с параметром О отображает окно Уназатель справки. Команда HELP_CONTEXT с парамет- ром, равным идентификатору темы, отображает тему с эеданным идентификатором (это тождественно рассмотренному ранее мето- ду HelpContext). Команда HELP CONTEXTPOPUP с парамет- ром, равным идентификатору темы, делает то же самое, но ото- бражает тему во всплывающем окне. Перед вызовом справки любым из рассмотренных методов возникает событие приложения OnHelp, которое может быть пе- рехвачено обработчиком аналогичного события компонента Ap- plicationEvents. Заголовок обработчика этого события имеет вид:
54 function TForml.ApplicationEventslHelp(Command: Hord; Data: Integer; var CallHelp: Boolean): Boolean; Параметр Command — это выполняемая команда, параметр Data — параметр этой команды, параметр CallHelp (по умолча- нию его значение равно true) определяет, будет ли после завер- шения обработчика вызвана программа справки WinHelp. Если зедать CallHelp = false, то WinHelp вызываться не будет. Обрабюотчик этого события можно использовать, чтобы что-то изменить в команде вызова справки. Приведем пример. Обработ- чик: function TForml.ApplicationEventslHelp( Command: Hord; Data: Integer; var CallHelp: Boolean): Boolean; begin if(Command=HELP_CCNTEXT) and (DataO) then begin Application.HelpCommand(HELP_CONTEXT₽OPUP,Data); CallHelp:=faIse; end; Resu1t:=true; end; обеспечивает отображение всех контекстных справок с номерами иденифнкаторов тем, меньшими 10, во всплывающем окне, не вызывая при этом WinHelp. Это дает возможность отобразить многострочные (в отличие от ярлычков) всплывающие окна, по- ясняющие назначение тек или иных элементов приложения. 1.10 Обеспечение бессбойной работы приложений 1.10.1 Исключения и способы борьбы с ними При работе программы могут возникать различного рода ошибки: переполнение, деление на нуль, попытка открыть несу- ществующий файл и т.п. При возникновении таких исключите- льных ситуаций программа генерирует так называемое исключе ние и выполнение дальнейших вычислений в данном блоке пре- кращается. Исключение — это объект специального вида, харак- теризующий возникшую в программе исключительную ситуа- цию. Он может также содержать в виде параметров некоторую уточняющую информацию. Особенностью исключений является то, что это сугубо временные объекты. Как только они обработа- ны каким-то обработчиком, они разрушаются. Если исключение не перехвачено нигде в программе (как это делать — будет рассказано в последующих разделах), то оно об-
Трабоипния к интерфейсу пользователи приди рабатывается методом TApplication.HandleException. Он обеспе- чивает стандартную реакцию программы на большинство исклю- чений — выдачу пользователю краткой информации в окне сооб- щений и уничтожение экземпляра исключения. На рис. 1.18 приведены примеры таких стандартных сообщений для случаев целочисленного деления на нуль и попытки открыть не сущест- вующий файл. Рие. 1Л8 Примеры стандартных сообще- нии об ошибках деления но нуль и ввода-вывода Если не принять соответствующих мер в случае генерации исключений, то к неприятностям прекращения вычислений мо- гут добавиться еще неприятности, связанные с так называемой утечкой, ресурсов. Под этим подразумеваются потери динамиче- ски распределяемой памяти, незакрытые файлы, не уничтожен- ные временные файлы на диске и прочий «мусор». Например, пусть вы выполняете некоторую программу, в которой имеются следующие операторы:
56 Рис. 1.19 Сообщение отладчике об исключении AssignFile (F,' а. tmp ’) ; Rewrite(F); New(P); Erase(F) ; Dispose(P) ; Вы открываете временный файл с именем a.trnp, чтобы хра- нить в нем какие-то промежуточные данные вычислений. В кон- це работы вы намерены уничтожить его процедурой Erase. Вы динамически выделяете некоторую память процедурой New, на- мереваясь освободить ее, когда она вам больше не будет нужна, процедурой Dispose. Но если в промежуточных операторах, по- меченных выше точками, возникнет исключение, то вычисления прервутся и процедуры Erase и Dispose не будут выполнены. В результате память, выделенная процедурой New, останется недо- ступной, а на диске сохранится временный и уже ненужный файл a.trnp. Помимо указанного, стандартная обработка исключений про- граммой имеет еще один недостаток — пользователь остается в полном недоумении, что же ему дальше делать? И не только не очень квалифицированный пользователь, которого приведенные ня рис. 1.18 сообщения на английском языке могут повергнуть в шок. Даже опытному человеку невозможно порой догадаться, что же в вашей программе делится на нуль и как этого можно из- бежать. Наверное, каждый попадал в подобные ситуации, даже применяя профессионально сделанные программы, включая Windows. Избежать всех этих неприятностей можно несколькими путя- ми: 1. Не допускать исключений, производя предварительную про- верку каждой операции, способной привести к исключитель- ным ситуациям 2. Защищать код зачистки «мусора* с помощью блока try ... fi- nally 3. Обрабатывать исключения в блоках try ... except
Требования к интерфейсу пользователя приложений для Window»57 Конечно, не всегда все это можно выполнить ня все 1UU про- центов. Каждый знает, что даже в Windows случаются аварий- ные завершения, после которых остаются неудаленные времен- ные файлы. Но, как говаривали древние: «Quod licet Jovi, non li- cet bovi», что» наверно, можно перевести на современный язык как: «Что дозволено Microsoft, не дозволено обычному програм- мисту». Так что если ваше приложение будет оставлять за собой «мусор», то вряд ли кто-нибудь захочет впредь иметь дело с ва- шими программами. 1.10.2 Не допускать исключений Первый из указанных в предыдущем разделе путей предусмат- ривает введение проверки перед выполнением каждой операции, способной привести к исключительным ситуациям. Например, пе- ред делением двух переменных А и В, из которых вторая может оказаться нулем, надо проверить значение В оператором if ( В - 0 ) then ... и выполнить необходимые действия, если переменная В окажет- ся равной нулю. Аналогично можно проверять возможность пе- реполнения при делении, умножении и других арифметических операциях, проверять наличие достаточного места в динамиче- ски распределяемой памяти при выделении новой области или создании нового объекта, проверять ошибки файлового ввода вы- вода. При использовании библиотечных функций вы не можете проверять отдельные операции, которые они проиаводят. В этом случае можно перед каждым обращением к функции или проце- дуре производить проверну допустимости всех ее аргументов. Достоинством рассмотренного подхода к устранению исклю- чений является «бессбойная» работа программы (если, конечно, вы проверяете действительно каждую операцию). К тому же, в безвыходных ситуациях, когда продолжать работу невозможно, вы перед ее прекращением можете выдать пользователю разум- ные советы, что же ему делать дальше. Конечно, надо понимать, что слово «бессбойная» недаром по- ставлено в кавычки. Сложные программы, увы, всегда содержат
58 ошибки и возможности сбоя. Даже тщательная предварительная проверка всех операций не способна это устранить. У вас всегда остается множество возможностей сделать ошибки в своей про- грамме, начиная с принципиальных погрешностей алгоритмов, и кончая тривиальным выходом индекса массива за допустимые пределы. Но все-таки многочисленные тщательные проверки всех операций повышают надежность программы. При несомненных достоинствах рассмотренного подхода у него есть ряд недостатков. Основной из них — увеличение разме- ра вашей программы (особенно, если она проводит много слож- ных вычислений) за счет многочисленных операторов проверки и замедление ее работы. К тому же при использовании библио- течных функций и процедур, реализация которых неизвестна, не всегда можно быть уверенным, что ваши проверки гарантируют их бессбойную работу. В нестоящее время проверку отдельных операции и функций на возмож- ность появления исключительных ситуаций надо признать устаревшим под- ходам, приводящим к неоправданному усложнению программы и снижению ее эффективности. Современный подход заключается в обработке исклю- чений. 1.10.3 Защита кода зачистки Второй способ борьбы с исключениями — использование бло- ка try ... finally. Блок, содержащий совокупность операторов, способных привести к исключению, можно оформить следую- щим образом: «операторы, способные привести к исключению и к появлению "мусора"> finally «операторы, выполняемые в любом случае и производящие зачистку "мусора"> end; В этом случае операторы в разделе finally будут выполняться всегда, независимо от того, было или не было исключение. После выполнения этих операторов вычисления, как и ранее, прерыва- ются. Если в блоке try вы открыли какие-то временные файлы, динамически выделили место под какне-то временные объекты, соединились с какими-то базами данных, а после всего этого про- изошла ошибка, вызвавшая исключение, то в блоке finally пы можете убрать весь «мусор»: удалить ненужные временные фай-
Требования к интерфейсу пшшэоептеля приложений для Windows 59 ли, освободить память от временных объектов, разорвать связь с базой данных. К сожалению, остаются другие из рассмотренных проблем: необходимость принять какие-то меры для дальнейшей нормаль- ной работы программы при генерации исключения, а также не- обходимость уведомить пользователя о желательных действиях с его стороны (сообщения типа приведенных на рис. 1.18 в этом случае отображаются на экране, но они мало информативны для пользователя). Решить эти проблемы в данном случае невозмож- но, поскольку при выполнении операторов раздела finally про- грамма не знает, произошло ли исключение, и если произошло, то какое именно. Рассмотренный блок try... finally позволяет защитить ресурсы внутри блока. Существует также способ глобального выделения и ими цианизации ресурсов, с защищенной их очисткой. Это до- стигается с помощью двух специальных включаемых в модуль разделов initialization и finalization. Раздел initialization, вы- полняемый один раз при первом упоминании в программе данно- го модуля, можно использовать для однократного выделения не- обходимых глобальных ресурсов. А раздел finalization, гаранти- рованно выполняемый в конце работы программы независимо от наличия или отсутствия исключений, можно использовать для зачистки этих и других ресурсов, затребованных при работе про- граммы. Например, если в процессе работы программы могут со- здаваться в каталоге, имя которого записано в переменной sdirtmp, временные файлы с расширениями .tmp, то гарантиро- ванно удалить их все при завершении работы программы можно следующими опараторами: var sSR:TSearchRec; F:File; ires:integer; initialization finalization ires := FindFirst(sdirtmp+’*.tmp’, faAnyFile, sSR); while ires=O do begin AssignFile(F,sdirtmp+sSR.name}; Erase(F); ires := FindNext(sSR) end;
Этот код с помощью процедуры Erase удаляет из каталога, имя которого записано в переменной sdirtmp, все файлы с рас- ширением .tmp. Поиск этих файлов осуществляется функциями FindFirst и FindNext. В разделе finalization можно определить, завершается ли про- грамма нормально или в результате генерации исключения, про- верив функцию ExceptAddr. IЛД, Т1 .1! Помните, что включение в модуль раздело finalization возможно только при наличии в нем раздело initialization. Даже, если он вам не нужен, вклю- чайте в программу хотя бы пустой раздел initialization, если хотите исполь- | i зоватъ зачистку в разделе finalization. 1.10.4 Обработка исключений в блоках try ... except 1.10.4.1 Синтаксис блоков try_except и операторов on...do Наиболее кардинальный путь борьбы с исключениями — об- работка их с помощью блоков try — except. Синтаксис этих бло- ков следующий: try {Исполняемый код} except {Код, исполняемый в случае ошибки) end; Операторы раздела except выполняются только в случае гене- рации исключения в операторах блока try- Таким образом, вы можете предпринять какие-то действия: известить пользователя о возникшей проблеме и подсказать ему пути ее решения, при- нять какие-то меры к исправлению ошибки (например, при деле- нии на нуль заслать в результат очень большое число соответст- вующего знака) и т.д. Наиболее ценным является то, что в разделе except вы можете определить тип сгенерированного исключения и дифференциро- ванно реагировать на различные исключительные ситуации- Это делается с помощью оператора: on <класс исключения> do <оператор>; В таблице 1.1 приведен краткий перечень наиболее часто встречающихся классов исключений.
Требошния к Интерфейсу пользователи приложений для Windows 61 Таблица 1.1. Некоторые классы исключений EDivByZero ElntOverflow Попытка целочисленного деления на нуль Переполнение при целочисленных операциях EZeroDivide Деление на нуль EOverflow Переполнение в операции с плавающей запятой; конкретная информация в поле Message ElnOutError Ошибка ввода/вывода из файла; поле errorcode со- держит информацию о конкретном виде ошибки EOlitOfMemory Не хватает места при динамическом распределении памяти EConvertError Ошибки преобразования в StrToInt, StrToFloat и др. Операторы оп...<1о позволяют проводить выборочную обработ- ку различных исключений. Приведем пример такой обработки: var А : shortint; try С := strTolnt(Edxtl.text); А := В div С; except on EConvertError do MessageDlg{ ’Вы ввели ошибочное число; повторите ввод", mtWarning, [mbOk], 0); on EDivByZero do MessageDlg('Вы ввели нуль; повторите ввод’. mtWarning, ImbOk], 0); on ElntOverflcw do if (B*C) >= 0 then A := 32767 else A := -32767; end; В этом примере вы осуществляете чтение целого числа, вве- денного пользователем в окно редактирования Editl, и делите на него переменную В. Если пользователь ввел ие целое число (на- пример, по ошибке нажал не цифру, а какой-то буквенный сим- вол), то при выполнении функции StrToInt возникнет исключе- ние класса EConvertError. Соответствующий обработчик исклю- чения сообщает пользователю о сделанной ошибке и советует по- вторить ввод. Аналогичная реакция следует на ввод пользовате- лем нуля (класс исключения EDivByZero) Если же при делении
f>2 возникает целочисленное переполнение (класс исключения Elnt- Overflow), то вы присваиваете результату максимальное положи- тельное или отрицательное значение. Некоторые исключения имеют дополнительные поля (свойст- ва), уточняющие вид ошибки. Например, это относится к исклю- чению файлового ввода/вывода ElnOntError, которое имеет свойство errorcode типа integer. Это свойство содержит стандарт- ный для Object Pascal номер ошибки файлового ввода/вывода. Чтобы воспользоваться полями исключений, оператор on надо записывать в виде on <имя>: «класс исключения> do «операторы с конструкциями <имя>.«имя поля»; Содержащееся в этом операторе имя носит сугубо локальный характер, нигде ранее определяться не должно и вводится только для того, чтобы можно было сослаться на поле по имени объекта исключения. Приведем пример использования полей при опера- циях файлового ввода/вывода (подразумевается, что в перемен- ной filename типа string хранится имя обрабатываемого файла): on IO: ElnOutError do begin Case IO.errorcode of 2: в:=’Файл +s+’” не найден’; 3: s:='Ошибочное имя файла +S+”1'; 4: s:='Слишком много открытых файлов1; 5: s:= 'Файл +з+” ' не доступен'; 100: s:= 'Достигнут конец файла "* +s+'"'; 101: s:= 'Диск переполнен при работе с файлом +S+’ " '; 106: s:= ’Ошибка ввода при работе с файлом +s+'”'; end; MessageDlg(s, mtWaming, [лйзОк], 0); end; Приведенный код анализирует поле исключения и выдает со- общение о виде ошибки. Помимо операторов on, обрабатывающих заданные типы иск- лючений, в раздел except может быть включен оператор else, в котором выполняется обработка всех не перехваченных ранее исключений, т.е. происходит обработка по умолчанию. Напри- мер: except on_____; on____; else «обработчик всех не перехваченных ранее себытий>; end;
Требовании к интерфейсу потжэоеателе гуилджехми для Window»63 Конечно, оператор else должен идти последним, так как в про- тивном случае все обработчики, записанные после него, работать ие будут, поскольку все исключения уже будут перехвачены. Внутри обработчика по умолчанию можно получить доступ к объекту исключения, воспользовавшись функцией ExceptObject. Следует указать на некоторую опасность применения else в этом контексте. Перехват всех исключений способен замаскиро- вать какие-то непредвиденные ошибки в программе, что затруд- нит их поиск и снизит надежность работы. В заключение описания синтаксиса обработки исключений надо отметить, что в раздел except могут включаться или только операторы оп, или только какие-то другие операторы. Смешение операторов on с другими не допускается. Раздел except может включаться совместно с описанным ра- нее разделом finally. Например, можно организовать блоки try следующим образом: try try finally except end; 1.10.4.2 Последовательность обработки исключений Блоки try—except могут быть вложенными явным или неяв- ным образом. Примером неявной вложенности является блок try-.except, в котором среди операторов раздела try имеются вы- зовы функций или процедур, которые имеют свои собственные блоки try—except. Рассмотрим последовательность обработки исключений в этих случаях. При генерации исключения сначала ищется соответствующий ему обработчик on в том блоке try—ex- cept, в котором создалась исключительная ситуация. Если соот- ветствующий обработчик не найден, поиск ведется в обрамляю- щем блоке try...except (при наличии явным образом вложенных блоков) и т.д. Если в данной функции или процедуре обработчик ие найден или вообще в ней отсутствуют блоки try—except, то по- иск переходит на следующий уровень — в блок, из которого была вызвана данная функция или процедура. Этот поиск продолжа- ется по всем урозиям. И только если он закончился безрезультат- но, выполняется стандартная обработка исключения, заключаю-
64 щаяся, как уже было сказано, в выдаче пользователю сообщения о типе исключения. Как только оператор on, соответствующий данному исключе- нию, найден и выполнен, объект исключения разрушается и управление передается оператору, следующему за соответствую- щим блоком try...except. Возможен также вариант, когда в самом обработчике исклю- чения в процессе обработки возникла исключительная ситуация. В этом случае обработка прерывается, прежнее исключение раз- рушается и генерируется новое исключение. Его обработчик ищется в блоке try...except, внешнем по отношению к тому, в ко- тором возникло новое исключение. На этом мы завершим рассмотрение исключений. Следует то- лько отметить, что имеется еще немало возможностей работы с исключениями: преднамеренная генерация исключений, созда- ние собственных исключений, отражающих какие-то типичные ошибочные ситуации в вашем приложении или неправомерные действия пользователя. Рассмотрение всех этих вопросов выхо- дит за рамки данной книги. Посмотрите соответствующую ин- формацию во встроенной справке Delphi. В ближайшей книге се- рии «Все о Delphi» — справочном пособии по Object Pascal, эти вопросы будут рассмотрены подробнее. 1.11 Печать из приложения текстов и изображений 1.11.1 Печать с помощью функций файлового ввода/вывода Печать в Delphi может осуществляться различными способа- ми. Простейший способ — использование обычных функций вво- да/вывода в текстовый файл, но связывание выходного потока не с файлом, а с принтером. Этот простейший способ можно исполь- вовать для получения оперативных распечаток протоколов како- го-то процесса. Предположим, например, что компьютер на заво- де подключен к строчному принтеру, чтобы сохранять данные по мере их накопления. Если в компьютере происходит аппаратный отказ, то все данные, собранные к этому моменту, имеются в рас- печаткех. Аналогично, вы, возможно, захотите напечатать про- стой список, которому не требуется графика, шрифты и другое форматирование, доступное в Windows. Код, использующий этом метод для печати текста» хранящегося в компоненте Editl, вы- глядит следующим образом: var Р : TextFile; begin
Требования к интерфейсу пользователя приложений для Windows 65 AssignFrn (Р); rewrite (Р); writein (P,Editl.Text); CloseFile (P); end; Здесь объявляется переменная P типа TextFile. Далее исполь- зуется разновидность команды Assign — AssignPrn. Она настра- ивает переменную Р на порт принтера, обращаясь с ним, как с файлом. Затем порт принтера должен быть открыт, что делается командой rewrite. Текст передается в принтер процедурой wri- tcln, а закрывается порт принтера командой CloseFile. Важно за- крыть порт принтера для завершения операции печати. По этой команде, как и в случае файла, любой текст, остающийся в памя- ти, пересылается в принтер, и порт закрывается. 1.11.2 Печать форм методом Print Формы в Delphi имеют метод Print, который печатает клиент- скую область формы. При этом полоса заголовка формы и полоса главного меню не печатается. Таким образом, можно включить в приложение форму, в которой пользователь во время выполне- ния размещает необходимые для печати результаты. Если имя этой формы Form2, то ее печать может выполняться оператором For m2.Print; Свойство формы PrintScale определяет опции масштабирова- ния изображения при печати. Возможные значения PrintScale: poNone poPrintToFit poProportional Масштабирование не используется. Размер изобра- жения может изменяться в зависимости от исполь- зуемого принтера Делается попытка напечатать изображение формы того же размера, который виден на экране Увеличивает ила уменьшает размер изображения, подгоняя его под размер страницы, заданный при установке принтера. Это значение принято по умол- чанию 1.11.3 Печать методом Print текстов в обогащенном формате Компонент RichEdit имеет метод Print, позволяющий печа- тать текст, хранящийся в компоненте. В этот метод передается единственный параметр типа строки, назначение которого за- ключается только в том, что при просмотре в Windows очереди
печатаемых заданий принтера эта строка появляется как имя за- дания. Например, оператор RichEditl.Print("Printing of RichEditl'>; обеспечивает печать текста компонента RichEditl. причем зада- ние на печать получает имя «Printing of RichEditl». Печать воспроизводит все заданные особенности форматиро- вания. Перенос строк и разбиение текста на страницы произво- дится автоматически. Длина строк никак не связана с размерами компонента Rich Edit, содержащего этот текст. Не только Rich- Edit, но и ряд других компонентов Delphi имеют методы печати. В частности, его имеют компоненты, предназначенные для ото- бражения графиков и диаграмм. Но их рассмотрение выходит за рамки данной книги. 1.11.4 Печать файлов средствами стандартных приложений Windows с помощью функции ShellExecute Для печати файлов средствами стандартных приложений Windows можно использовать функцию ShellExecute. Чтобы воспользоваться функцией ShellExecute, надо в оператор uses вашего приложения добавить ссылку на модуль ShellApi. Функ- ция ShellExecute при соответствующем задании ее параметров ищет по расширению заданного для печати файла соответствую- щую ему системную программу Windows, и, если находит, то осуществляет печать. Например, обычно Windows настроен так, что файлам с расширением .txt соответствует программа Note- pad, а файлам с расширением .doc — Word. В этом случае выпол- нение оператора ShellExecute(Handle, "print", ’test.txt",nil,nil,SW_Hide}: вызовет печать файла с именем test.txt средствами программы Notepad, а оператор ShellExecute(Handle, "print', ’teat.doc",nil,nil,SW_Hide); вызовет печать файла с именем test.doc средствами программы Word. Этот способ печати можно использовать как для распечатки заранее созданных файлов, так и для распечатки файлов, создан- ных во время выполнения приложения методами SaveToFile, имеющимися у многих компонентов. 1.11.5 Печать с помощью объекта Printer В Delphi имеется класс печатающих объектов TPrinter, кото- рый обеспечивает печать текстов, изображений и других объек- тов, расположенных на его канве — Canvas. Свойства канвы по- дробно рассмотрены в разделе 6-1.3.
Требования к интерфейсу пользователя прмло; Модуль Delphi, именуемый Printers, содержит переменную Printer, являющуюся объектом типа TPrinter. Чтобы ее исполь- зовать, надо добавить модуль Printers в оператор uses вашей про- граммы. Автоматически он ие добавляется. Рассмотрим некоторые свойства и методы объекта типа TPrin- ter. Свойство, метод Описание Canvas Канва Canvas — место в памяти, в котором форми- руется страница или документ перед печатью TextUut Метод канвы, который позволяет посылать в нее текст BeginDoc Используется для начала задания печати EndDoc Используется для окончания заданна печати. Фак- тическая печать происходит только при вызове EndDoc PageHeight Возвращает высоту страницы в пикселях NewPage Принудительно начинает новую страницу на при- нтере PageNomber Возвращает текущий номер печатаемой страницы Предположим, вы хотите напечатать текст, используя печата- ющий объект. Вы можете написать код вида: Printer.BeginDoc; Prin ter.Canvas.TextOut(10,10, 'Я печатаю через объект Printer'}: Printer-EndDoc; Этот код вызывает печать на канве принтера текста «Я печатаю через объект Printer», начиная с десятого пикселя слева и десятого сверху. BeginDoc запускает задание на печать. Текст посылается на канву с помощью метода TextOut объекта Canvas- Метод End- Doc вызывает печать текста и останавливает задание на печать. Если вы хотите напечатать изображение, хранящееся в ком- поненте Imagel, это можно сделать операторами: Printer.BeginDocr with Imagel-Picture.BitMap do Printer.Canvas.CopyRect(Rept CO,0» Height,Width) , Canvas,Rect(0,0,Height,Width)); Printer.EndDoc; Печатающий объект Printer не производит автоматического переноса строк и разбиения текста на страницы. Поэтому печать длинных текстов с помощью объекта Printer требует достаточно
68 Глава 1 сложного программирования. Проще это делать описанными ра- нее способами или с помощью встроенной в Delphi системы Quick- Report. Но ее рассмотрение выходит за рамки данной книги.
Глава 2 Проектирование окон с изменяемыми размерами 2.1 Выравнивание компонентов — свойство Align Если проектируется окно, размеры которого пользователь мо- жет изменять во время выполнения приложения, то необходимо принять меры, чтобы компоненты в окне при этом тоже изменя- ли свои размеры или местоположение, равномерно распределя- ясь по площади окна и не оставляя пустых мест. Пусть, например, вы проектируете форму, окно которой со- держит панель Panell, на которой будут размещаться управляю- щие компоненты и список ListBoxl, панель Рапе12, в середине которой будет размещаться некоторая надпись в метке Static- Textl, и компонент Memol, в котором будут редактироваться тексты (рис. 2.1 а). Если, разместив их на форме, вы не примете мер для того, чтобы при изменении размеров окна компоненты изменялись, то при том размере формы, который вы проектиро- вали, все будет выглядеть нормально (рис. 2.1 а). Но если пользо- ватель растянет размеры формы, надеясь увеличить площадь ре- дактирования и длину списка, то приложение приобретет неле- пый вид, показанный на рис. 2.1 б. Увеличение формы просто приводит к увеличению на ней пустого места. Рис. 2.1 Результоты изменения размеров окна при неправильном проектировании
70 Глава 2 Чтобы избежать таких неприятностей, у многих компонентов и, в частности, у панелей, есть свойство Align — выравнивание. По умолчанию оно равно alNone, что означает, что никакое вы- равнивание не осуществляется. Но его можно задать равным al- Тор, или alBottom, или alLeft, или alRigbt, что будет означать, что компонент должен занимать всю верхнюю, или нижнюю, или левую, или правую часть клиентской области компонен- та-контейнера. Под клиентской областью понимается вся свобод- ная площадь формы или другого контейнера, в которой могут размещаться включенные в этот контейнер компоненты. Можно также задать свойству Align компонента значение alClient, что приводит к заполнению компонентом всей свободной клиентской области. Во всех этих случаях размеры компонента будут автома- тически изменяться при изменении размеров контейнера. В приведенном ранее примере логично для панели Рапе12 за- дать значение Align, равное alTop, чтобы ширина панели авто- матически менялась с изменением ширины окна. Панель займет всю верхнюю часть формы, а ее высота будет такой, какую вы установите. Для панели Panel 1 следует задать значение Align, равным alLeft, так как увеличение ее ширины не имеет смысла, а увеличение высоты позволяет увидеть большую часть списка ListBoxl. Компонент Panel 1 займет всю левую часть клиентской области, оставшейся свободной после размещения Рапе12. Для компонента Meiuol следует задать значение Align, равным alCli- ent, что позволит компоненту увеличиваться и в высоту, и в ши- рину, увеличивая площадь редактирования. При этом компонент Memol займет всю площадь клиентской области, оставшуюся по- сле размещения Panell и Рапа12. В результате во время выпол- нения при изменении пользователем размеров окна приложение будет иметь вид, представленный на рисунке 2.2. Задавать компонентам соответствующие значения свойства Align в приведенном примере надо именно в указанной выше по- следовательности: alTop для Panel2, alLeft для Panell, alClient Рис. 2.2 Изменение роэмеров панелей при использовании выравнива-
для Memol. Если, например, начать с задания alClient для Me- mol. то компонент Memol займет всю площадь формы и осталь- ные компоненты вообще не будут видны. Значения свойства Align имеют определенный приоритет: значения аГГор и alBottom имеют более высокий приоритет, чем значения alLeft и alRight. Поэтому не любые варианты размеще- ния и выравнивания панелей можно реализовать непосредствен- но. Например, пусть вы хотите создать форму, вид которой при- веден на рис. 2.3 а. Причем вам надо, чтобы при изменении раз- меров окна панель Panell продолжала занимать всю левую часть формы (т.е. надо задать Align alLeft), Panels занимала бы всю нижнюю часть площади, свободной от Panell (т.е. для Рапе13 надо задать Align = alBottom), а Рапе12 занимала бы всю остав- шуюся часть клиентской области (т.е. для Рапе12 надо задать Align = alClient)- Напрямую все это сделать невозможно. Если вы задали для Panell значение alLeft, а затем для Рапе13 задаете значение alBottom, то в силу приоритета alBottom панель Pa- nels займет всю нижнюю часть формы, вытеснив оттуда Panell. В подобных случаях приходится вводить дополнительные па- нели-контейнеры. В приведенном примере на форме сначала надо разместить Panell и дополнительную панель Рапе14, как показано на рис. 2.3 б. Для Panell задается alLeft, а для Рапе14 задается alClient. Затем панели Рапе12 и Рапе13 размещаются на Рапе14 и для них задаются значения: alBottom для Рапс13 (по- скольку клиентской областью для Рапе13 является область пане- ли Рапе14, то Рале13 займет нижнюю часть правой половины эк- Рис. 2.3 Проектировоние с учетом при- оритетов значений Align
72 Глади 2 рана) и значение alClient для Рапе12. В результате получим нуж- ное выравнивание всех панелей. 2.2 Изменение местоположения и размеров компонентов Заданием значений Align задачу планировки окна с изменяю- щимися размерами еще нельзя считать решенной- Вернемся к ранее рассмотренному примеру. Рис. 2.2 показал, что в нем при изменении размеров окна метка StaticTextl, остающаяся на мес- те, перестает быть в середине панели, а размер списка ListBoxl не изменяется. Чтобы устранить эти недостатки, можно воспользоваться свойствами компонента, определяющими его привязку, местопо- ложение и размеры. Начиная с Delphi 4 оконные компоненты имеют свойство Anc- hors — привязку к родительскому компоненту при изменении размеров последнего. Это свойство представляет собой множест- во, которое может содержать следующие элементы: акТор Верхний край компонента привязан к верхнему краю ро- дительского компонента akLeft Левый край компонента привязав к левому краю родите- льского компонента akRight Правый край компонента привязан к правому краю роди- тельского компонента akBottom Нижний край компонента привязан к нижнему краю ро- дительского компонента Если в множестве Anchors присутствуют привязки к противо- положным сторонам родительского компонента, то при измене- нии размеров родительского компонента происходит растяжение или сжатие дочернего компонента, поскольку расстояния от сто- рон родительского компонента выдерживаются. Сжатие может происходить вплоть до полного исчезновения изображения дан- ного компонента. Поэтому необходимо ограничивать диапазон допустимого изменения размеров. Способы такого ограничения будут рассмотрены несколько позднее. По умолчанию привязка осуществляется к левому и верхнему краям родительского компонента. Т.е. по умолчанию свойство Anchors равно [akLeft,акТор]. Если задать н свойстве Anchors привязку к противоположным сторонам родительского компо-
Проектирование окон с изменяемыми размерами нента, то при изменении размеров родительского компонента бу- дут меняться размеры и данного компонента. Если для списка ListBoxl в нашем примере задать свойство Anchors равным [ак- Left,akTop,akBottom], то при изменении высоты панели, содер- жащей этот список, будут поддерживаться постоянными рассто- яния верхнего и нижнего краев списка соответственно от верхне- го и нижнего краев панели. Таким образом, увеличивая высоту окна, пользователь может увеличивать число строк, видимых в списке без прокрутки. Если задать свойство Anchors равное [ак- Left,akTop, akRight] для метки StaticTextl, то при изменении ширины содержащей ее панели будут поддерживаться постоян- ными расстояния левого и правого краев метки от соответствую- щих краев панели. Метка будет оставаться в центре панели, но ее размер будет изменяться. Вариант нашего примера с заданными описанным способом свойствами Anchors для ListBoxl и Static- Textl, приведен на рис. 2.4. Как видно из него, теперь приложе- ние сохраняет нормальный вид и при увеличении пользователем размеров окна. А в списке ListBoxl при увеличении размера даже исчезла полоса прокрутки, т.к. весь текст уместился на эк- ране. Рис. 2Л Изменение роэмеров компонентов при использовонии привязки Anchors Для того, чтобы это нормально работало, надо еще в обработ- чик события OnResize формы или панели, содержащей метку StaticTextl, вставить оператор StaticTextl.Repaint; Этот оператор обращается к методу Repaint, который перери- совывает метку. Если этого не сделать, то, как вы легко можете проверить, при изменении размеров окна в метке появится над- пись на новом месте, но не исчезнет и прежняя надпись, т.е. по- лучится сдвоенная надпись, что, конечно, недопустимо. Не всегда все проблемы, связанные с перестроением компо- новки окна при изменении его размера, удается решить с помо-
74 Глава 2 щью свойства Anchors. Даже в приведенном примере, вероятно, не очень оправдано изменение размера метки StaticTextl. Если свойство Anchors не справляется с размещением компонентов, надо переходить к программному изменению их местоположения и размеров. Это необходимо делать и в ранних версиях Delphi, в которых свойство Anchors отсутствует. Местоположение компонента задается свойствами Left и Тор, харвктеризующими координаты левого верхнего утла компонен- та. При этом началом координат считается левый верхний угол клиентской области компонента-контейнера. Горизонтальная ко- ордината отсчитывается вправо от этой точки, а вертикальная — вниз. Размеры компонента определяются свойствами Width — ши- рина и Height — высота. Есть еще одно свойство BonndsRect, ха- рактеризующее одновременно и местоположение, и размеры компонента, но оно менее удобно и используется редко. Имеется еще два свойства, определяющие размер клиентской области компонента-контейнера: Clientwidth и ClientHeight. Для некоторых компонентов они совпадают со свойствами Width и Height. Но могут и отличаться от них. Например, в форме Cli- entHeight меньше, чем Height, за счет бордюра, полосы заголов- ка, меню и полосы горизонтальной прокрутки, если она имеется. Если надо изменять местоположение и размеры компонентов при изменении размеров формы, то соответствующие операторы следует разметить в обработчике события формы OnResize. Это событие возникает при любом изменении пользователем разме- ров окна приложения. Чтобы избежать возможного несоответст- вия расположения компонентов при исходных и измененных размерах окна, полезно сослаться на тот же обработчик и в собы- тии формы OnShow. В рассмотренном ранее примере, если не ис- пользовать свойство Anchors, то тех же (и даже несколько луч- ших) результатов можно добиться включением в обработчик со- бытия формы OnResize операторов: StaticTextl.Left := Panel2.Left + (PanelZ.ClienWidth — StaticTextl-Width) div Z; ListBoxl.Height := Panell-ClientHeight — ListBoxl.Top — 20; Первый из этих операторов сдвигает левый край компонента StaticTextl так, чтобы метка всегда размещалась в середине па- нели Рапе12. А второй оператор изменяет высоту списка List- Boxl при изменении высоты панели Panell.
с шмонаемыми рсшлерами 2.3 Панели с перестраиваемыми границами В ряде случаев описанного автоматического изменения разме- ров различных панелей оказывается недостаточно для создания удобного пространства для действий пользователя. Даже при увеличении окна на весь экран квкая-то панель приложения мо- жет оказаться перегруженной информацией, а другая относите- льно пустой. В этих случаях полезно предоставить пользователю возможность перемещать границы, разделяющие различные па- нели, изменяя их относительные размеры. Пример такой воз- можности можно увидеть в программе Windows «Проводник», Начиная с Delphi 3 в библиотеке имеется специальный компо- нент — Splitter на странице Additionol, который позволяет легко осуществить это. При работе с ним надо соблюдать определенную последовательность проектирования. Если вы хотите установить Splitter между двумя панелями, первая из которых будет выров- нена к какому-то краю клиентской области, а вторая займет всю клиентскую область, то сначала надо выровнять первую панель, например, влево. Затем надо перенести на форму Splitter и вы- ровнять его в ту же сторону (тоже влево). Splitter прижмется к соответствующему краю первой панели. После этого можно за- дать выравнивание второй панели на всю оставшуюся площадь клиентской области. В результате Splitter окажется зажатым между двумя панелями и при запуске приложения он позволит пользователю изменять положение соответствующей границы между этими панелями. Подобные разделители Splitter можно разместить между все- ми панелями приложения, дав пользователю полную свободу из- менять топологию окна, с которым он работает. Компонент Splitter имеет событие OnMoved, которое наступа- ет после конца перемещения границы- В обработчике этого собы- тия надо предусмотреть, если необходимо, упорядочение размеще- ния компонентов на панелях, размеры которых изменились: пере- местить какие-то метки, изменить размеры компонентов и т. д. Свойство ResizeStyle компонента Splitter определяет поведе- ние разделителя при перемещении его пользователем. Поэкспе- риментируйте с ним, чтобы увидеть различие в режимах переме- щения разделителя. По умолчанию свойство Splitter равно rsPattern. Это означает, что пока пользователь тянет курсором мыши границу сам разделитель не перемещается и панели тоже остаются прежних размеров. Перемещается только шаблон ли- нии, указывая место намечаемого перемещения границы. Лишь после того, как пользователь отпустит кнопку мыши, раздели- тель переместится и панели изменят свои размеры. Практически
U Глава 2 такая же картина наблюдается, если установить ResizeStyle = rsLine. При ResizeStyle = rsUpdate в процессе перетаскивания границы пользователем разделитель тоже перемещается и разме- ры панелей все время меняются. Это, может быть, удобно, если пользователь хочет установить размер панели таким, чтобы на ней была видна какая-то конкретная область. Но так квк процесс перетаскивания в этом случае сопровождается постоянной пере- рисовкой панелей, наблюдается неприятное мерцание изображе- ния. Так что этот режим можно рекомендовать только в очень редких случаях. Если установить ResizeStyle - rsNone, то в про- цессе перетаскивания границы не перемещается ни сама грани- ца, ни изображающая ее линия. Вряд ли это удобно пользовате- лю, так что я не могу представить случая, когда было бы выгодно использовать этот режим. Свойство MinSize компонента Splitter устанавливает минима- льный размер в пикселях обеих панелей, между которыми зажат разделитель. Задание такого минимального размера необходимо, чтобы при перемещениях границы панель не сжалась бы до нуле- вого размера или до такой величины, при которой на ней исчезли бы какие-то необходимые для работы элементы управления. К сожалению, в версиях Delphi, младше Delphi 5, свойство MinSize не всегда срабатывает верно. В Delphi 5 введено новое свойство компонента Splitter — AutoSnap. Если оно установлено в true (по умолчанию), то при перемещении границы возможны те же неприятности, что в младших версиях Delphi. Но если устано- вить AutoSnap в true, то пермещение границы панелей сверх пределов, при которых размер одной из панелей станет меньше MinSize, просто блокируется. Твк что можно рекомендовать всегда устанавливать AutoSnap в true. Впрочем, и это не решает всех задач, связанных с перемещением границ панелей. Дело в том, что свойство MinSize относится к обеим панелям, граница между которыми перемещается, а в ряде случаев желательно раздельно установить различные минимальные размеры одной и другой панели. Это проще сделать, задав в панелях соответствую- щие значения свойства Constraints, о котором будет рассказано в следующем разделе. 2.4 Ограничение пределов изменения размеров окон и компонентов Все рассмотренные ранее методы изменения размеров панелей и компонентов на них имеют общий недостаток: при чрезмерном уменьшении пользователем размеров окна какие-то компоненты
Проектировшче окон с hmwwmcihmmm размерами 77 могут исчезать из поля зрения. Иногда к некрасивым с точки зрения эстетики результатам приводит и чрезмерное увеличение размеров окна. Хотелось бы иметь средства, ограничивающие по- льзователя в его манипуляциях с окном и не позволяющие ему чрезмерно уменьшать и увеличивать размеры. Начиная с Delphi 4 такие средства имеются. Это свойство Con- straints, присущее всем оконным компонентам и позволяющее задавать ограничения на допустимые изменения размеров. Свой- ство имеет четыре основных подсвойства: MaxHeight, Max- Width, MinHeight и Min Width — соответственно максимальная высота и ширина и минимальная высота и ширина. По умолча- нию значения всех этих подсвойств равны О, что означает отсут- ствие ограничений. Но задание любому из этих свойств положи- тельного значения приводит к соответствующему ограничению равмера заданным числом пикселей. Чтобы какие-то компоненты не исчезали из поля зрения, можно задать им ограничения минимальной высоты и длины. Таким образом можно поддерживать нормальные пропорции от- дельных частей окна. Можно задать ограничения на минималь- ные и максимальные размеры формы, т.е. всего окна. Например, если вы зададите для формы значения MaxHeight ---- 500 и Max- Width = 500, то пользователь не сможет сделать окно большим, чем квадрат 500 х 500. Причем это ограничение будет действо- вать, даже если пользователь нажмет системную кнопку, разво- рачивающую окно на весь экран. Окно развернется, но его разме- ры не превысят заданных. Это иногда полезно делать, чтобы раз- вернутое окно не заслонило какие-то другие нужные пользовате- лю окна. В более ранних версиях Delphi свойство Constraints отсутству- ет. Работая с этими версиями можно ограничивать размеры окна, пользуясь стандартными сообщениями Windows. Приведем без особых пояснений код, позволяющий ограничить размер окна: type TForml = class(TForm) private procedure WMGetMinMaxInfo(var InfozTWMGetMinMaxInfo); message WM_GETMINMAXINFO; implementation procedure TForml-WMGetMinMaxInfo(var Info: TWMGetMinMaxInfo);
begin with Info.MinMaxInfo'' do begin ptMinTrackSize.x := 150; ptMaxTrackSize.x := 300; ptMinlrackSize.y := 100; ptMaxTrackSize.y := 200; ptMaxPosition-x:=BoundsRect.Left; ptMaxPosition.у:=BoundsRect.Top; end; inherited; end; Этот код ограничивает возможности пользователя по измене- нию размеров окон пределами 100-5-200 пикселей в высоту (значе- ния ptMinTrackSize.y и ptMaxTrackSize.y) и 150^-300 пикселей в ширину (значения ptMinTrackSize.x и ptMaxTrackSize.x), а также задает координаты левого верхнего угла распахнутого окна (ptMaxPosition.x и ptMaxPosition.y) равными текущим ко- ординатам левого верхнего угла окна. Таким образом этот код не только ограничивает размеры окна, но и позволяет задать поло- жение полностью развернутого окна на экране. 2.5 Масштабирование компонентов Говоря о размерах компонентов, следует упомянуть еще об од- ной возможности их изменения — о методе ScaleBy. Он опреде- лен как: procedure ScaleBy(М, D: Integer); где М и D — множитель и делитель, определяющие изменение масштаба компонента. Масштабируются такие свойстза компо- нента, как Width и Height, определяющие его размер. Свойства Тор и Left остаются неизменными. Масштабируется также раз- мер шрифта, если только в компоненте не установлено Parent- Fon = true. В последнем случае шрифт наследуется от родитель- ского компонента и поэтому из изменяется. Если компонент является контейнером, содержащим другие компоненты, то его дочерние компоненты также масштабируют- ся. Причем у них изменяются не только Width и Height, но так- же пропорционально изменяются Тор и Left, определяющие их местоположение. Если во всех дочерних компонентах установле- но ParentFont - true, а в компоненте-контейнере ParentFont = false, то пропорционально изменяются и шрифты всех компонен- тов (но, конечно, не непрерывно, а скачками, доступными тому или иному типу шрифта).
Проектировоннв окон с изменяемыми роамерими 79 Параметры М and D определяют соответственно множитель и делитель масштаба. Например, чтобы уменьшить размеры на 10% от начального значения, можно задать М равным 9, a D рав- ным 10 (9/10). Если же вы хотите увеличить размер на 1/3, то можно задать М=133 и D=100 (133/100) или МН4 и D=3 (4/3). Приведем примеры. Оператор Editl.ScaleBy(11,10) ; масштабирует окно редактирования Editl. В любом случае при выполнении этого оператора длина окна (свойство Width) увели- чивается на 10%, что обеспечивает возможность наблюдать и ре- дактировать в нем более длинный текст. Высота окна (свойство Height) будет изменяться пропорционально, только если свойст- во компонента AutoSize равно false. В противном случае высота определяется только размером шрифта и при постоянном шриф- те будет неизменной. А размер шрифта будет меняться, только если свойство компонента ParentFont равно false, т.к. иначе шрифт определяется родительским компонентом. Таким образом при AutoSize — true и ParentFont = true изменяется только дли- на окна редактирования, т.е. приведенный выше оператор экви- валентен оператору Editl-Width := ll’Editi-Width div ID; но компактнее. Подобный оператор можно использовать для предоставления пользователю возможности самому менять длину окна редакти- рования. Можно ввести для окна, например, комбинации кла- виш Alt+U и Alt+D, нажимая которые пользователь будет при необ- ходимости увеличивать или уменьшать его длину. Чтобы сделать это, можно написать следующий обработчик события ОпКеуИр (события, связанные с клавиатурой, рассмотрены в разделе 3.2): procedure TForml-EditlKeyUp(Sender:TObject;var Key:Word; Shift: TShiftState) begin if (Key = ord(*U’)> and (ssAlt in Shift) then , Editl.ScaleBy(11,10) else if (Key = ordl'D')) and (ssAlt in Shift) then Editl.ScaleBy(10,11) end; Когда Editl находится в фокусе, при нажатии пользователем клавиши Alt и клавиши U (в любом регистре и независимо от пе- реключения на латинский или русский язык) длина окна редак- тирования увеличится на 10%, а при нажатии All и D соответст- венно уменьшится.
во Установив для окна редактирования ParentFont = false, можно получить одновременно с изменением размера окна и изменение размера шрифта. Это может быть полезно пользователю, поскольку зрение у всех разное и пользователю может захотеться, например, увеличить размер символов. На рис. 2.5 приведено три варианта окна. Размеры вфхнего остаются постоянными, какими были за- даны во время проектирования всем трем окнам. Среднее окно уве- личено описанным выше способом. Поскольку у него ParentFont = true, то размер шрифта и высота при этом не изменились. Нижнее окно увеличено настолько же, но у него ParentFont = false. Поэто- му размер шрифта и высота окна тоже возросли. Рис. 2.5 |Голеннаео Гул Примеры масштабирований окна при различных .•Я».’ " значениях свойства ParentFont |Голямлеъ-Куист |голенищев-Кутузов Приведем еще примеры. Оператор Panell-ScaleBy(11,10) ; увеличивает размер панели Panell, а также координаты и раз- мер всех ее компонентов. Если в панели ParentFont = false, то шрифты во всех компонентах также увеличиваются независимо от значений их свойства ParentFont. Причем они увеличиваются и в неоконных компонентах, например, в метках. Если же в па- нели ParentFont = true, то шрифты увеличиваются только в компонентах, в которых ParentFont = false. Приведенный выше оператор изменяет масштаб сразу группы компонентов, но при этом сдвигает их позиции, поскольку дейст- вует на свойства Тор и Left. Можно избежать этого, обращаясь по отдельности к каждому дочернему оконному компоненту па- нели с помощью, например, такого оператора: var ind:integer; for ind := 0 to Panell.Controlcount — 1 do if (Panell.Controls[ind] is TWinControi) then (Panell.Controls[ind] as TWinControi).ScaleBy(11,10); При этом надо иметь в виду, что непосредственно применять ScaleBy можно только к оконным компонентам, являющимся на- следниками класса TWinControi. Поэтому в приведенном опера- торе проверяется, является ли компонент наследником TWinCon- troi, и обращение к нему производится тоже квк к компоненту типа TWinControi. Надо также учитывать, что неоконные компо- ненты, налример, метки, при этом масштабироваться не будут.
Глава 3 Обработка событий клавиатуры и мыши Все действия пользователя при взаимодействии с приложени- ем сводятся к перемещению мыши нажатию кнопок мыши и на- жатию клавиш клавиатуры. Рассмотрим обработку в приложе- нии событий, связанных с этими манипуляциями пользователя. 3.1 События мыши 3.1.1 Последовательность событий В компонентах Delphi определен ряд событий, связанных с мышью. Основные из этих события: Событие Описание OnClick Щелчок мыши на компоненте и некоторые другие ; действия пользователя. OnDblClick Двойной щелчок мыши на компоненте OnMouseDown Нажатые клавиши мыши над компонентом. Возмож- но распознавание нажатой кнопки и координат кур- сора МЫШИ. OnMouseMove Перемещении курсора мыши над компонентом. Воз- можно распознавание нажатой кнопки и координат курсора мыши. OnMonseUp Отпускание ранее нажатой кнопки мыши над ком- понентом. Возможно распознавание нажатой кнопки и координат курсора мыши. On Start Drag Начало процесса «перетаскивания» объекта. Воз- можно распознавание перетаскиваемого объекта. OnDragOver Перемещение «перетаскиваемого» объекта над ком- понентом. Возможно распознавание перетаскиваемо го объекта и координат курсора мыши. OnDragDrop Отпускание ранее нажатой кнопки мыши после «пе- ретаскивания» объекта. Возможно распознавание пе- ретаскиваемого объекта и координат курсора мыши.
83 Глава 3 Событие Описание OnEndDrag Еще одно событие при отпускании ранее нажатой кнопки мыши после «перетаскивания» объекта. Воз- можно распознавание перетаскиваемого объекта и координат курсора мыши. OnEnter Событие в момент получения элементом фокуса в ре- зультате манипуляции мышью, нажатия клавиши табуляции или программной передачи фокуса. OnExit Событие в момент потери элементом фокуса в резу- льтате манипуляции мышью, нажатия клавиши та- буляции или программной передачи фокуса. | Как видно из приведенной таблицы, эти события охватывают все мыслимые манипуляции с мышью и даже дублируют многие из них. Наиболее широко используется событие OnClick. Обычно оно наступает, если пользователь щелкнул на компоненте, Т.е. нажал и отпустил кнопку мыши, когда указатель мыши нахо- дился на компоненте. Но это событие происходит также и при не- которых других действиях пользователя. Оно наступает, если: Пользователь выбрал элемент в сетке, дереве, списке, выпада- ющем списке, нажав клавишу со стрелкой. Пользователь нажал клавишу пробела, когда кнопка или ин- дикатор были в фокусе. Пользователь нажал клавишу Enter, а активная форма имеет кнопку по умолчанию, указанную свойством Default. Пользователь нажал клавишу Esc, а активная форма имеет кнопку прерывания, указанную свойством Cancel. Пользователь нажал клавиши быстрого доступа к кнопке или индикатору. Например, если свойство Caption индикатора ва писано как 'ЛПолужирный' и символ ’ГТ подчеркнут, то нажа- тие пользователем комбинации клавиш Alt+П вызовет событие OnClick в этом индикаторе. Приложение установило в true свойство Checked радиокноп- ки RadioButton. Приложение изменило свойств Checked индикатора Check- Box. Вызван метод Click элемента меню. Для формы событие OnClick наступает, если пользователь щелкнул на пустом месте формы или на недоступном компонен- те.
Обработки событий клавиатуры и мыши 83 Обилие событий, связанных с мышью, а также фактическое дублирование некоторых из них требуют четкого представления о последовательности отдельных событий, наступающих при том или ином действии пользователя. Рассмотрим эти последователь- ности. В момент запуска приложения из рассматриваемых нами со- бытий наступает только событие OnEnter в компоненте, на кото- рый передается фокус. Это событие не связано с какими-то дейст- виями пользователя, так что не будем на нем останавливаться. Теперь рассмотрим простейшее действие пользователя: пере- ключение с помощью мыши фокуса с одного элемента на другой. Последовательность событий в этом случае приведена в табли- це 3.1: Таблица 3.1. Последовательность событий мыши при переключении фокуса Действие пользователя Событие Перемещение курсора мыши в пределах первого компонента Множество событий OnMouseMove в первом компоненте Перемещение курсора мыши в пределах формы Множество событий OnMouseMove в форме Перемещение курсора мыши в пределах второго компонента Множество событий OnMouseMove во втором компоненте । Нажатие кнопки мыши OnExit в первом компоненте OnEnter во втором компоненте OnMouseDown во втором компоненте Отпускание кнопки мыши OnClick во втором компоненте OnMouseUp во втором компоненте События OnMouseMove происходят постоянно в процессе пе- ремещения курсора мыши и даже просто при его дрожании, не- избежном, если пользователь не снимает руки с мыши. Это надо учитывать и пользоваться этим событием очень осторожно, по- скольку оно, в отличие от других, происходит многократно. Как видно из приведенной таблицы, каждое действие пользо- вателя, связанное с нажатием или отпусканием кнопки мыши, приводит к серии последовательно наступающих событий. В об- работчиках событий OnMonseDown и OnMonseUp можно распо- знать, какая кнопка мыши нажата и в какой точке компонента находится в данный момент курсор мыши.
84 Глава 3 Рассмотренная в таблице 3.1 последовательность событий имеет место, если во втором компоненте свойство DragMode (см. раздел 4.1) равно dmManual (ручное начало процесса перетаски- вания), как это установлено по умолчанию. Если же это свойство равно dmAntomatic (автоматическое начало процесса перетаски- вания), то все рассмотренные события, связанные с манипуля- цией мышью, заменяются следующими: OnMouseDown Заменяется на OnStartDrag OnMouseMove Заменяется на событие OnDragOver того компонен- та. над которым перемещается курсор мыши OnMouseUp Заменяется на событие OnDragDrop компонента, над которым завещается перетаскивание (если ком- понент может воспринять информацию от перетас- киваемого объекта), и последующее событие OnEnd Drag компонента, который перетаскивался События OnExit и OnEnter вообще не возникают, поскольку переключения фокуса не происходит. Не наступает также собы- тие OnClick. В дальнейшем в данном разделе события, связанные с перета- скиванием, рассматриваться не будут. Подробное описание этих событий см. в разделе 4.1. Если в примере, приведенном в таблице 3.1, щелчок делается на объекте, который уже находится в этот момент в фокусе, то не происходят события OnExit и OnEnter. В этом случае при нажа- тии кнопки наступает только событие OnMoHeeDown а при отпу- скании кнопки — события OnClick и OnMouseUp. Теперь рассмотрим последовательность событий при двойном щелчке на компоненте. Она приведена в таблице 3.2. Распозна- вать нажатую кнопку мыши по-прежнему можно только в собы- тиях OnMouseDown и OnMouseUp. Если же надо распознать именно двойной щелчок какой-то определенной кнопкой мыши, то можно, например, ввести некую переменную, являющуюся флагом двойного щелчка, устанавливать этот флаг в обработчике события OnDblClick, а в обработчиках событий OnMouseDown или OnMouseUp проверять этот флаг и, если он установлен, то сбрасывать его и выполнять запланированные действия.
Обработка событий клавиатуры и мыши Таблица 3.2. Последовательность событий мыши при двойном щелчке на компоненте Дествие пользователя Событие Первое нажатие кнопки мыши OnMouseDown. Возможно распознавание на- | жатой кнопки и координат курсора мыши OnMouseUp. Возможно распознавание и той кнопки и координат курсора мыши Первое отпускание OnClick кнопки мыши Второе нажатие кнопки мыши OnDblCliek OnMouseDown. Возможно распознавание н, жатой кнопки и координат курсора мыши Второе отпускание OnMouseUp. Возможно распознавание наж кнопки мыши | той кнопки и координат курсора мыши. 3.1.2 Распознавание источника события, нажатых кнопок и клавиш, координат курсора Во все обработчики событий, связанных с манипуляциями мыши (как и во все другие обработчики), передается параметр Sender типа TObject. Этот параметр содержит указатель на ком- понент, в котором произошло событие. Он не требуется, если пи- шется обработчик события для одного конкретного компонента. Однако часто один обработчик применяется для нескольких ком- понентов. При этом какие-то операции могут быть общими для любых источников события, а какие-то требовать специфических действий. Тогда Sender можно использовать для распознавания источника события. Правда, поскольку тип TObject не имеет ни- каких полезных для пользователя свойств и методов, то объект Sender следует рассматривать как объект одного из производных от TObject типов. Например, если требуется распознать только тип объекта, можно использовать операцию is. Так оператор if (Sender is TListBox) then __ проверяет, не является ли источник события компонентом типа TListBox. Если требуется распознать объект по имени или другому свой- ству, можно использовать операцию as. Например, оператор if (Sender as TControl).Name = 'Editl' then ... проверяет, не является ли этот компонент компонентом с именем Editl. Впрочем, в данном случае можно было бы просто опреде- лить, не является ли Sender объектом Editl:
В6 Глоно 3 if (Sender = Editl) then ... Операцию as можно также использовать, если надо приме- нить к источнику события некоторый метод. Например, оператор (Sender as TWinCcntrol).ScaleBy(11,10); увеличивает на 10% размер любого оконного компонента, в кото- ром произошло обрабатываемое событие. Помимо параметра Sender в обработчики событий OnMouse- Down и OnMonseUp передаются параметры, позволяющие распо- знать нажатую кнопку, нажатые при этом вспомогательные кла- виши, а также определить координаты курсора мыши. Заголовок обработчика события OnMouseDown или OnMonseUp может иметь, например, следующий вид: procedure TForml.Edit1МоиseDown(Sende г: TObject; Button: TMouseButton; Shift: TShiftState; X, ¥: Integer); Помимо уже рассмотренного нами параметра Sender в обра- ботчик передаются параметры Button, Shift, X и Y. Параметр Button типа TMouseButton определяет нажатую в этот момент кнопку мыши. Тип TMouseButton — перечислимый тип, определяемый следующим образом: TMouseButton = (mbLeft, iribRight, mbMiddle); Значение mbLeft соответствует нажатию левой кнопки мыши, значение mb Right — правой, а значение mbMiddle — средней. Например, если вы хотите, чтобы обработчик реагировал на на- жатие только левой кнопки, вы можете его первым оператором написать: if (Button о mbLeft) then exit; Тогда, если значение Button не равно mbLeft, т.е. нажата не левая кнопка, выполнение обработчика прерается. Параметр Shift типа TShiftState определяет, какие вспомога- тельные клавиши на клавиатуре нажаты в момент нажатия кнопки мыши. Тип TShiftState — множество, определенное сле- дующим образом: TShiftState = set of (ssShxft, ssAlt, ssCtrl, ssLeft, ssRight, ssMiddle, ssDouble); Элементы этого множества соответствуют нажатию клавиш Shift (ssShift), Alt (ssAlt), Ctrl (ssCtrl), а также кнопок: левой (ssLeft), правой (ssRight), средней (ssMiddle). Информация о на- жатых кнопках в параметрах Button и Shift имеет различный смысл. Параметр Button соответствует кнопке, нажимаемой в данный момент, а параметр Shift содержит информацию о том, какие кнопки были нажаты, включая и те, которые были нажа-
Обработка событии клавиатуры и мыши В7 ты ранее. Например, если пользователь нажмет левую кнопку мыши, а затем, не отпуская ее, нажмет правую, то после первого нажатия множество Shift будет равно (ssLeftJ, а после второго — [ssLeft, ssRight]. Если до этого пользователь нажал и не отпус- тил какне-то вспомогательные клавиши, то информация о них также будет присутствовать в множестве Shift. Поскольку Shift является множеством, проверять наличие в нем тех или иных элементов надо операцией in. Например, если вы хотите прореагировать на событие, заключающееся в нажа- тии левой кнопки мыши при нажатой клавише Alt. можно испо- льзовать оператор: if (Button - mbLeft) and (ssAlt in Shift} then — В приведенной структуре if первое условие (Button = mbLeft) можно заменить эквивалентным ему условием, проверяющим параметр Shift: if (ssLeft in Shift) -- Аналогичные параметры Button и Shift передаются и в обра- ботчик события OnMouseUp. Отличие только в том, что параметр Button соответствует не нажимаемой в данный момент, а отпус- каемой кнопке. Параметр Shift передается твкже в обработчик события OnMouseMove, так что и в этом обработчике можно определить, какие клавиши и кнопки нажаты. Во все события, связанные с мышью, передаются также коор- динаты курсора X и Y. Эти параметры определяют координаты курсора в клиентской области компонента. Благодаря этому можно обеспечить различную реакцию в зависимости от того, в какой части клиентской области расположен курсор. Пусть, на- пример, у вас в приложении имеется компонент Imagel, содер- жащий некоторое изображение, и вы хотите обеспечить пользо- вателя соответствующими пояснениями в зависимости от того, на какую часть изображения он покажет мышью- Это можно сде- лать, например, следующим образом (см. рис. 3.1). Для перехода в режим получения информации можно преду- смотреть на форме кнопку или раздел меню, при выборе которых параметру Cursor задается значение crHelp (это соответствует изображению курсора с вопросительным знаком, как на рис. 3.1): procedure TForml.SpeedButtonlClick(Sender: TObject); begin Cursor := crHelp; end; Для отображения информации можно предусмотреть на фор- ме, например, окно редактирования Memol (можно было бы еде-
В8 Глава 3 Пример приложения, использующего координаты мыши лить красивее, отображая информацию во всплывающих окнах, как описано в разделе 1.9, но не будем усложнять наш пример). В событие OnMouseDown компонента Imagel и формы можно ввести обработчик вида: procedure TForml.IrnagelNouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if (Sender <> Imagel) then Cursor := crDefault; If (Cursor <> crHelp) then exit; Memol.Lines.Clear; if (Y < 120)then if (X < 160} then Memol.Lines.Add(’...’) else if (X < 270) then Cursor := crDefault; end; Первые операторы этого обработчика обеспечивают выход из процедуры, если вид курсора не crHelp или щелчок произведен не на изображении. Следующий оператор очищает окно редакти- рования от прошлой информации, а затем структура if анализи- рует координаты курсора и в соответствии с областью, в которой находится курсор, отображает ту или иную информацию о час- тях изображения.
Оброботки событий клен натуры и мыши 3.2 События клавиатуры 3.2.1 Последовательность событий В оконных компонентах Delphi определены три события, свя- занные с клавиатурой. Это события: Событие Описание OnKeyDown Событие наступает при нажатии пользователем любой клавиши. Можно распознать нажатые клавиши, вклю- чая функциональные, и кнопки мыши, но нельзя рас- познать символ нажатой клавиши OnKeyPress Событие наступает при нажатии пользователем клави- ши символа. Можно распознать нажатую клавишу символа, различить символ в верхнем и нижнем реги- стре. различить символы кириллицы и латинские, но нельзя распознать функциональные клавиши и кноп- ки OnKeyUp Событие наступает при отпускании пользователем лю- бой клавиши. Можно распознать нажатые клавиши, включая функциональные, и кнопки мыши, но нельзя распознать символ отпускаемой клавиши Кроме того, при нажатии пользователем клавиши табуляции фокус может переключаться с элемента на элемент, что вызывает описанные в разделе 3-1-1 события OnEnter и OnExit. Важно четко представлять последовательность событий, про- исходящих при нажатии пользователем клавиши или комбина- ции клавиш. Пусть, например, пользователь нажал клавишу Shift (неравен ввод в верхний регистр), а затем нажал клавишу симво- ла «н». Последовательность событии для этого случая приведена в таблице 3.3- В таблице указано, что именно можно распознать при каждом событии. Подробнее это будет рассмотрено ниже, а пока отметим, что различить символ в верхнем и нижнем регист- рах и различить латинский символ и символ кириллицы можно только в обработчике события OnKeyPress. Действительно, хотя в событии OnKeyDown при нажатии клавиши «н» можно опреде- лить, что при этом одновременно нажата и клавиша Shift, этого еще мало, чтобы утверждать, что символ относится к верхнему регистру. Ведь если перед этим была включена клавиша Caps- Lock, то при нажатой клавише Shift символ окажется в нижнем ре- гистре. А информация о том, включена или выключена клавиша CapsLock, в обработчик события OnKeyDown не передается.
90 Глово3 Таблица 3.3. Последовательность событий клавиатуры при нажатии клавиш Shift-H Действие пользователя Событие j Нажатие клавиши Shift OnKeyDown. Возможно распознавание нажатой клавиши Shift Нажатие клавиши *н» OnKeyDown. Возможно распознавание нажатой клавиши Shift, нажатой клави- ши «н», но отличить верхний регистр от нижнего и латинский символ от русско- го невозможно । OnKeyPress. Возможно распознавание символа с учетом регистра и языка, но невозможно распознавание нажатой кла- виши Shift Отпускание клавиши «н» OnKeyUp. Возможно распознавание на- жатой клавиши Shift, отпущенной клави- ши «н», но отличить верхний регистр от нижнего и латинский символ от русско- го невозможно Отпускание клавиши Shift OnKeyUp. Возможно распознавание от- пущенной клавиши Shift Следует отметить, что событие OnKeyPress заведомо наступа- ет, если нажимается только клавиша символа или клавиша сим- вола при нажатой клавише Shift. Если же клавиша символа нажи- мается одновременно с какой-то из вспомогательных клавиш, то событие OnKeyPress может не наступить (произойдут только со- бытия OnKeyDown при нажатии и OnKeyUp при отпускании) или, если и наступит, то укажет на неверный символ. Например, при нажатой клавише Alt событие OnKeyPress при нажатии сим- вольной клавиши не наступает. А при нажатой клавише Ctrl со- бытие OnKeyPress при нажатии символьной клавиши наступает, но символ не распознается. В заключение надо остановиться на вопросе, куда поступают события клавиатуры. У формы имеется свойство KeyPreview. Оно влияет на обработку событий, поступающих от клавиатуры (в число этих событий не входит нажатие клавиш со стрелками, клавиш табуляции и т.п.). По умолчанию свойство KeyPreview равно false и события клавиатуры поступают на обработчики, предусмотренные в активном в данный момент компоненте. Но если задать значение KeyPreview равным true, то сначала эти со- бытия будут поступать на обработчики формы, если таковые пре-
Обработка событии клавиатуры и мыши 91 дусмотрены, и только потом поступят на обработчики активного компонента. Имеется также событие On Shortcut приложения (Applica- tion), которое возникает при нажатии пользователем клавиши. Событие возникает до того, как возникло стандартное событие OnKeyDown компонента или формы. Это событие, как и все со- бытия приложенния, перехватывает введенный в Delphi 5 компо- нент ApplicationEvents. Обработчик этого события позволяет предусмотреть нестандартную реакцию на нажатие какой-то клавиши. В него передается параметр сообщения Windows Msg, поле CharCode которого (Msg.CharCode) содержит виртуальный код нажатой клавиши- Передается также по ссылке параметр Handled. Если задать ему значение true, то стандартные события OnKeyDown, OnKeyPress, OnKeyUp не наступят. Пусть, например, вы хотите, чтобы при нажатии пользрвате- лем клавиши «Q» приложение закрывалось, спросив предварите- льно пользователя, действительно ли он хочет кончить работу. Тогда обработчик события OnShortCut компонента Application- Events может иметь вид: procedure TForml.ApplicationEventslShortCut( var Msg: TWMKey; var Dandled: Boolean}; begin if Msg.CharCode = Ord('Q') then if Application.MessageBox{ 'Действительно хотите завершить работу?', 1 Подтвердите завершение’, MB_YESNOCANCEL+MB_ICONQUESTION) = IDYES then Application.Terminate; end; Если пользователь нажал клавишу с символом «Q» (в любом регистре и независимо от установки русского или английского языка — см. подробнее о распознавании клавиш в разделе 3-2.2), то пользователю методом Application.MessageBox предлагается диалоговое окно с запросом о завершении работы. Если пользова- тель в нем нажмет кнопку До, то приложение закрывается мето- дом Application.Tenninate. 3.2.2 Распознавание нажатых клавиш Заголовок обработчика события OnKeyDown может иметь, на- пример, следующий вид: procedure TForml.Edi tlMouseDown(Sender: TObject; var Key: Word; Shift: TShiftState) ;
92 Параметр Sender, указывающий на источник события, уже обсуждался выше при описании событий мыши (см. раз- дел 3.1.2). Там же рассматривался и параметр Shift, представля- ющий собой множество элементов, отражающих нажатые в это время функциональные клавиши. Только в обработчике события OnKeyDown множество возможных элементов параметра Shift сокращено до ssShift (нажата клавиша Shift), ssAlt (нажата кла- виша Alt) и ssCtrl (нажата клавиша Ctrl). Информация о нажатых кнопках мыши отсутствует. Основной параметр, которого не было раньше — это параметр Key. Обратите внимание, что он определен как var, т.е. может изменяться в обработчике события. Кроме того, обратите внима- ние, что это целое число, а не символ. Параметр Key определяет нажатую в момент события клави- шу клавиатуры. Для не алфавитно-цифровых клавиш использу- ются виртуальные коды API Windows. Полную таблицу этих ко- дов вы можете посмотреть во встроенной справке Delphi- Ниже в таблице 3.4 приведены для дальнейшего обсуждения только не- сколько строк из нее, соответствующих наиболее распространен- ным клавишам. Таблице 3.4. Некоторые коды клавиш Клавиша Десятичное число Шестнадцате- ричное число Символиче- ское имя Сравнение no функции ord F1 112 $70 VKJF1 Enter 13 $0D VKJRETURN Shift 16 $10 VK SHIFT Ctrl 17 $11 VK CONTROL Aft 18 $12 VK MENU Esc 27 IB VKJESCAPE 0.) 48 $30 ord( O’) 1 1 49 $31 ord('l') n,N,T,T 78 $4E ord('N') y,Y,H,H 89 $59 ordfr) Параметр Key является целым числом, определяющим клави- шу, а не символ. Например, один и тот же код соответствует про- писному и строчному символам *У» и «у». Если, как это обычно бывает, в руссцрй г^лари^ууре этой клавип^ соответствуют сщи-,.
Обработке событий кжжиотуры ы мыши волы кириллицы «Н» и «н», то их код будет тем же самым. Раз- личить прописные и строчные символы или символы латинские и кириллицы невозможно. Проверять нажатую клавишу можно, сравнивая Key с целым десятичным кодом клавиши, приведенным во втором столбце таблицы 3.4- Например, реакцию на нажатие пользователем кла- виши Enter можно оформить оператором: if (Key = 13) then ... ; Можно сравнивать Key и с шестнадцатеричным эквивалентом кода, приведенным в третьем столбце таблицы 3.4. Например, приведенный выше оператор можно записать в виде: if (Key - $0D) then ... : Для клавиш, которым ие соответствуют символы, введены так- же именованные константы, которые облегчают написание про- граммы, поскольку ие требуют помнить численные коды клавиш. Например, приведенный выше оператор можно записать в виде: if (Key = VKRETURN) then ... ; Для клавиш символов и цифр можно производить проверку сравнением с десятичным или шестнадцатеричным кодом, но это не очень удобно, так как трудно помнить коды различных симво- лов. Другой путь — воспользоваться функцией ord, определяю- щей код символа. Коды латинских символов в верхнем регистре совпадают с виртуальными кодами, используемыми в параметре Key. Поэтому, например, если вы хотите распознать клавишу, соответствующую символу «Y», вы можете написать: if (Key = ord(’Y') then ... ; В этом операторе можно использовать только латинские сим- волы в верхнем регистре. Если вы напишете ordfy') или захоти- те написать русские символы, соответствующие этой клавише — ord('H') или огб('н'), то оператор на сработает. Помните также, что оператор будет действовать на все симво- лы, относящиеся к указанной клавише: «Y*, «у», «Н» и «н». Ино- гда это хорошо, а иногда плохо. Например, если вы задали пользо- вателю какой-то вопрос, на который он должен ответить Y (да) или N (нет), то подобный оператор избавляет пользователя от необхо- димости следить, в каком регистре он вводит символ и какой язык — английский или русский включен в данный момент. Ему доста- точно просто нажать клавишу, на которой написано «Y». Однако, если пользователь более привык к символам кириллицы, то могут возникнуть неприятности, поскольку нажимая клавишу с латин- ской буквой «У» и русской буквой «Н» он может думать, что отве- чает ие положительно (Yes — да), а отрицательно (Нет).
94 Глава 3 Приведем еще один пример — автоматическую передачу фо- куса очередному оконному компоненту при нажатии пользовате- лем клавиши Enter. Это можно сделать, включив в общий обработ- чик событий OnKeyDown всех оконных компонентов оператор: if (Key = VK-RETURN) then FindNextControl{Sender as TWinControl, true, true, false).SetFocus; Этот оператор с помощью метода FindNextControl ищет оче- редной компонент в последовательности табуляции и передает ему фокус. Подробнее этот пример и метод FindNextControl рас- смотрен в разделе 1-8. В заключение приведем пример распознавания комбинации клавиш. Пусть вы хотите, например, распознать комбинацию Alt-X. Для этого вы можете написать оператор: if((Key = ord(’X'))and (ssAlt in Shift)) then _ ; Мы рассмотрели распознавание клавиш при событии OnKey- Down. Заголовок обработчика события OnKeyUp имеет такой же вид, так что все сказанное в равной степени относится и к собы- тиям при отпускании клавиш. Теперь перейдем к рассмотрению события OnKey Press. Заго- ловок обработчика этого события имеет вид: procedure (Sender: TObject; var Key: Char); В этот обработчик, как и в описанные выше, передается пара- метр Key, определяющий нажатую клавишу символа. Но обрати- те внимание, что тип этого параметра ие целое число, как в пре- дыдущих случаях, a Char — символ. В данном случае в обработ- чик передается ие виртуальный код клавиши, а символ, по кото- рому можно определить, прописная это буква, или строчная, рус- ская, или латинская. Поэтому описанных выше сложностей с распознаванием символов не возникает. Пусть, например, вы задали пользователю вопрос, на который он должен ответить символами «Д» или ед» (да), или символами ♦ Н» или *н» (нет). Тогда распознать положительный ответ в обра- ботчике события OnKeyPress можно оператором: if {(Key = 'Д’) or (Key - ’д’)) then .. Этот оператор можно записать короче, воспользовавшись опе- рацией in: if (Key in [’Д’, ’л'1) then Приведенные операторы реагируют только на положительный ответ пользователя, не реагируя на отрицательный или ошибоч-
95 Обработке событий клоииатуры и мыши ный ответ. Реакцию на все возможные ответы обеспечивает структура case: case Key of ’Д’,'д’: Н'.'н': end; Здесь предусмотрена реакция на положительный и отрицате- льный ответ, а также звуковой сигнал при ошибочном ответе. Посмотрев на приведенный ранее заголовок обработчика, вы можете увидеть, что параметр Key передается как var. Это позво- ляет в обработчике изменять этот параметр, изменяя соответст- венно его стандартную обработку в компоненте, поскольку ваш об- работчик события срабатывает раньше стандартного обработчика компонента. Пусть, например, вы хотите иметь на форме окно ре- дактирования Editl, в котором пользователь должен вводить толь- ко целые числа без знака, разделенные запятыми или пробелами. Вы можете обеспечить безошибочный ввод, подменяя все недопус- тимые символы нулевым с помощью, например, такого оператора: if not {Key in ['О’..’9', ' *, then Key := #0; При нажатии пользователем любой клавиши, кроме клавиш с цифрой, запятой или пробелом, символы подменяются нулевым символом и просто не появляются в окне редактирования, как вы можете убедиться, сделав приложение с этим простым приме- ром. Можно добавить в обработчик звуковой сигнал при нажатии пользователем ошибочной клавиши: if not (Key in {'О’.-’Э’, ’ then begin Key := #0; Beep; end;

Глава 4 Перетаскивание объектов 4.1 Перетаскивание информации об объектах — технология Drag&Drop Процесс перетаскивания с помощью мыши информации из од- ного объекта в другой (Drag&Drop), коротко называемый перета- скиванием, очень широко используется в Windows. Например, вы можете перемещать файлы между папками, перемещать сами папки, включая их в другие папки, и т.д. Посмотрим, как осуще- ствляется подобное перетаскивание информации объектов в Del- phi. Все свойства, методы и события, связанные с процессом пере- таскивания, определены в классе TControl, являющемся праро- дителем всех визуальных компонентов Delphi. Поэтому они яв- ляются общими для всех компонентов. Начало процесса перетаскивания определяется свойством DragMode, которое может устанавливаться в процессе проекти- рования или программно равным dmManual или dmAutomatic. Значение dmAutomatic (автоматическое) определяет автоматиче- ское начало процесса перетаскивания при нажатии пользовате- лем кнопки мыши над компонентом. Имейте в виду, что в этом случае событие OnMouseDown, связанное с нажатием пользова- телем кнопки мыши, для этого компонента вообще не наступает. Значение же dmManual (ручное) говорит о том, что начало про- цесса перетаскивания должен определять программист. Для это- го он должен в соответствующий момент вызвать метод Begin- Drag. Например, он может поместить вызов этой функции в об- работчик события OnMouseDown, наступающего в момент нажа- тия кнопки мыши. В этом обработчике он может проверить пред- варительно какие-то условия (режим работы приложения, нажа- тие тех или иных кнопок мыши и вспомогательных клавиш) и при выполнении этих условий вызвать BeginDrag. Пусть, например, процесс перетаскивания должен начаться, если пользователь нажал левую кнопку мыши и клваишу Alt над списком ListBoxl Тогда свойство DragMode этого компонента надо установить в dmManual, а его обработчик события OnMou- seDown может иметь вид:
98 Глаяо 4 procedure TForml.ListBoxlMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if (Button = mbLeft) and (ssAlt in Shift) then ListBox1.BeginDrag(false); end; Параметр Button обработчика события OnMouseDown пока- зывает, какая кнопка мыши была нажата, а параметр Shift яв- ляется множеством, содержащим обозначения нажатых в этот момент кнопок мыши и вспомогательных клавиш клавиатуры (см. раздел 3.1.2). Приведенный выше оператор проверяет, нажа- та ли левая кнопка мыши и клавиша Alt. Если нажаты, то вызы- вается метод BeginDrag данного компонента. В предыдущем примере в функцию BeginDrag передано зна- чение false. Это означает, что процесс перетаскивания начнется не сразу, а только после того, как пользователь сдвинет мышь с нажатой при этом кнопкой. Это позволяет отличить простой щелчок мыши от начала перетаскивания. Если же передать в Be- ginDrag значение true, то перетаскивание начнется немедленно. Когда начался процесс перетаскивания, обычный вид курсора изменяется. Пока он перемещается над формой или компонента- ми, которые не могут принять информацию, он обычно имеет тип- crNoDrop: Если же он перемещается над компонентом, гото- вым принять информацию из перетаскиваемого объекта, то при- обретает вид, определяемый свойством перетаскиваемого объек- та DragCorsor. По умолчанию это свойство равно crDrag, что со- ответствует изображению 11. Надо подчеркнуть, что вид курсора определяется свойством DragCorsor перетаскиваемого объекте, а на того объекта, над которым перемещается курсор- В процессе перетаскивания компоненты, над которыми пере- мещается курсор, могут информировать о готовности принять информацию от перетаскиваемого объекта. Для этого в компо- ненте должен быть предусмотрен обработчик события OnDrag- Over, наступающего при перемещении над данным компонентом курсора, перетаскивающего некоторый объект. В этом обработ- чике надо проверить, может ли данный компонент принять ин- формацию перетаскиваемого объекта, и, если не может, задать значение false передаваемому в обработчик параметру Accept. По умолчанию этот параметр равен true, что означает возмож- ность принять перетаскиваемый объект. Обработчик для списка ListBoxl может иметь, например, следующий вид:
Перетоскивпние об-ьектои procedure TForml.ListBoxlDragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); begin if(Source <> Sender) then Accept := Source is TListBox else Accept := false; end; В нем сначала проверяется, ие являются ли данный компо- нент (Sender) и перетаскиваемый объект (Source) одним и тем же объектом. Это сделано, чтобы избежать перетаскивания ин- формации внутри одного и того же списка. Если источник и при- емник являются одним и тем же объектом, то срабатывает else и параметр Accept становится равным false, запрещая прием ин- формации. Если же это разные объекты, то Accept делается рав- ным true, если источником является какой-то другой список (компонент класса TListBox), и равным false, если источник яв- ляется объектом любого другого типа. Таким образом компонент ListBoxl сообщает, что готов принять информацию ив любого другого списка. Значение параметра Accept, задаваемое н обработчике собы- тия OnDragOver, определяет вид курсора, перемещающегося при перетаскивании над данным компонентом. Этот вид показывает пользователю, может ли данный компонент принять перетаски- ваемую информацию. Если в компоненте не описал обработчик события OnDragOver, то считается, что данный компонент не мо- жет принять информацию перетаскиваемого объекта. Процедура приема информации от перетаскиваемого объекта записывается в обработчике события OnDragDrop принимающе- го компонента. Это событие наступает, если после перетаскива- ния пользователь отпустил кнопку мыши над данным компонен- том. В обработчик этого события передаются параметры Source (объект-источник) и X и Y координаты курсора. Если продол- жить начатый выше пример перетаскивания информации из од- ного списка в другой, то обработчик события OnDragDrop может иметь вид: procedure TForml.Lis tBoxlDragDrop{Sender, Source: TObject; X, Y: Integer); begin (Sender as TListBox). I terns. Add ( (Source as TListBox).Items[ (Source as TListBox).Itemindex]) ;
100 В этом обработчике строка, выделенная в списке-источнике (Source as TListBox).Items[(Source as TListBox).ItemIndex], до- бавляется в список-приемник методом (Sender as TList- Box).Items.Add. Используется операция as, позволяющая рас- сматривать периметры Sender и Source как указатели на объек- ты класса TListBox. Это делается потому, что эти параметры объ- явлены в заголовке процедуры как указатели на объекты класса TObject. Но в классе TObject нет свойств Items и Itemindex, ко- торые нам требуются. Эти свойства определены в классе TList- Box, являющемся наследником TObject. Поэтому с параметрами Sender и Source в данном случае надо оперировать как с указате- лями на объекты TListBox, что и выполняет операция as. Конечно, в данном случае можно было бы ие использовать па- раметр Sender, заменив (Sender as TListBox) просто на ListBoxl. Но запись оператора в общем виде с помощью параметра Sender позволяет воспользоваться таким обработчиком и для других компонентов ListBox, если они имеются в приложении. После завершения или прерывания перетаскивания наступает событие OnEndDrag, в обработчике которого можно предусмот- реть какие-то дополнительные действия. Имеется также связан- ное с перетаскиванием событие OnStartDrag, которое позволяет произвести какие-то операции в начала перетаскивания. Это со- бытие полезно при автоматическом начале перетаскивания, ког- да иным способом этот момент нельзя зафиксировать. Теперь, объединив приведенные выше фрагменты обработки перетаскивания, просуммируем, что надо сделать, если вы имее- те в приложении несколько списков ListBox и хотите обеспечить возможность копирования строк каждого из этих списков в лю- бой другой. Это потребует двух операций: Выделите все списки на форме как единую группу и напишите для них приведенный выше единый обработчик события OnD- ragOver. Аналогичным образом напишите для списков приведенный выше единый обработчик события OnDragDrop. И это все! Для обеспечения возможности перетаскивания вам потребовалось написать всего два оператора. Если вы хотите на- чинать перетаскивание только при выполнении какого-то допол- нительного условия, например, при нажатии клавиши Alt, то вам потребуется сделать еще один шаг: Задайте для всех списков значение свойства DragMode, рав- ное dmManual. Напишите для всех списков приведенный выше единый обработчик события OnMouseDown.
Перетаскивание объектов ТОТ Теперь вы можете создать форму с несколькими списками, за- нести в них информацию, написать эти обработчики и проверить все это на практике. На рис. 4.1 приведен такой пример, демон- стрирующий различные аспекты перетаскивания. Попробуйте для тренировки сами создать аналогичный, позволяющий пере- таскивать метки и строки из одного списка в другой и из списков в окно редактирования Мето. Окно, сообщающее о результатах перетаскивания, которое вы видите на рис. 4.1, генерируется в обработчиках событий OnEndDrag перетаскиваемых компонен- тов кодом: if Target = nil then ShowMessage(’Перенесение объекта '+ (Sender as TControl).Name + ' прервано') else ShowMessage((Sender as TContrcl).Name -V 1 перенесен в ' + (Target as TControl).Name); end; Рис. 4.1 Пример приложения, использующего Drog&Drop 4.2 Перетаскивание и встраивание объектов — технология Drag&Doc. Плавающие окна Начиная с Delphi 4 реализована новая технология перетаски- вания и встраивания оконных объектов — Drag&Doc. Вы можете познакомится с результатами этой технологии, работая с Интег-
102 Глава 4 рированной Средой Разработки Delphi 4. Она предоставляет по- льзователю полную свободу в перестройке интерфейса. Отдель- ные окна могут объединяться вместе, создавать многостраничное окно с закладками, затем опять разъединяться и т.д. Посмотрим, как подобный подход можно реализовать в своем приложении. У оконных компонентов введено свойство DockSite, которое по умолчанию равно false, но если его установить в true, то ком- понент становится контейнером: приемником, способным прини- мать переносимые в него компоненты — клиенты. Еще одно свойство приемника — UseDockManager. Если оно равно true (это принято по умолчанию), то процессом встраивания клиента автоматически управляет диспетчер встраивания, соответствую- щий тому компоненту, который является приемником. Если же задать UseDockManager равным false, то встраивание клиентов ложится на плечи программиста. В компонентах — потенциальных клиентах надо установить свойство DragKind в dkDock. Кроме того, если вы хотите, чтобы процесс перетаскивания начинался автоматически, то так же, как и в технологии Drag&Drop, надо установить свойство Drag- Mode в dmAntomatic. Если оставить значение DragMode равным dmManual по умолчанию, то управление процессом Drag&Doc осуществляется так же, как описывалось ранее для процесса Drag&Drop. Посмотрим, к чему все это может привести. Давайте построим тестовое приложение, в котором осуществим Drag&Doc, не напи- сав ни одной строчки кода. Начните новое приложение. Перенесите на форму 6 панелей, расположив их примерна так, как показано на рис. 4.2 а. В пане- ли Panell установите свойство DockSite в true. Эта панель смо- жет служить у вас приемником. В панелях Рапе12 — Рпе16 уста- новите DragKind в dkDock и DragMode в dmAntomatic. Запусти- те приложение. Попробуйте перетаскивать мышью панели Ра- пе!2 — Рпе16. Вы увидите (рис. 4.2 б), что панель Panell может принимать другие панели, размещая их внутри себя, причем спо- соб встраивания зависит от того, в какой части приемника вы за- вершаете буксировку клиента. Между размещенными клиента- ми имеются разделители, которые позволяют вам перемещать буксировкой границы между клиентами, изменяя их относитель- ные размеры. Вы можете также видеть, что даже независимо от наличия или отсутствия в приложении приемника, при щелчке на компонентах-клиентах они превращаются в самостоятельные плавающие окна (панели Рапе15 и Рапе16 на рис. 4.2 б), которые можно перемещать, выходя даже за пределы родительской фор-
Ператочсиионие объектов 103 мы, можно изменять их размеры, можно закрывать их, после чего соответствующие компоненты исчезают. Причем все это до- стигнуто только заданием соответствующих свойств, без единого написанного вами оператора. На рис. 4.2 б вы можете наглядно наблюдать, что плавающее окно теряет связь со своим прежним контейнером. В приведен- ном примере свойства ParentFont всех панелей равнялись true, а на форме был установлен жирный шрифт. Как видно из рис. 4.2 б, встроенные панели сохраняют жирный шрифт своего контейне- ра, а в плавающих окнах шрифт перестал быть жирным. Рис. 4.2 °) Демонстрации техники Drog&Doc форма [а) и окно прило- жения во время выпол- нения (б) Приемником может быть и сама форма. Укажите для формы вашего приложения DockSite равным true и выполните прило- жение опять. Поведение перетаскиваемых панелей несколько из- менится. При щелчке на компонентах-клиентах они по-прежне- му превращаются в плавающие окна, которые можно переме- щать, изменять их размеры и т.п. Но при повторном щелчке они опять становятся обычными панелями, сохраняя при этом изме- ненное местоположение и размеры. Объясняется это тем, что по- вторный щелчок воспринимается в этом случае как встраивание клиента в приемник — форму. Теперь посмотрим, как можно управлять из компонента-при- емника всеми этими процессами. У приемника имеется свойство DockClients, объявленное как Dockclients[Index: Integer] : TControl; которое позволяет получить доступ к каждому клиенту, перене- сенному в приемник. Свойство DockClientCount содержит число клиентов. Оба свойства — DockClients и DockClientConnt только для чтения. Определены соответствующие события, которые позволяют приемнику управлять процессом встраивания. Они генерируют- ся только в том случае, если в компоненте установлено DockSite = true.
104 Глава 4 Первое из этих событий — OnGetSiteInfo, возникающее в мо- мент начала перетаскивания и повторяющееся непрерывно в процессе перетаскивания. Заголовок соответствующего обработ чина имеет вид: procedure TForml.PanellGetSitelnfo(Sender: TObject; DockClient: TControl; var InfluenceRect: TRect; MousePos: TPoint; var CanDock: Boolean); В качестве параметров в обработчик передается DockClient — перетаскиваемый объект, InfluenceRect — прямоугольник рам- ки перетаскиваемого объекта, который можно изменять, Mouse- Pos — положение курсора мыши и CanDock — разрешение пере- таскивания. Если в обработчике указать CanDock = false, это бу- дет означать, что приемник отказывается принимать компонент. Например, вы можете написать в обработчике события OnGetSi- telnfo панели Panell оператор: CanDock := Dockclient о Panelfi; Тогда Panell будет принимать любой компонент, кроме Ра пе!6. Еще одно событие — OnDockOver, возникающее и повторяю- щееся, когда компонент-клиент перемещается над компонен- том-приемником, точнее, когда в поле приемника войдет курсор мыши, буксирующий клиента. Это событие аналогично событию OnDragOver в технологии Drag&Drop. Заголовок соответствую- щего обработчика имеет вид: procedure TForml.PanellDockOver(Sender: TObject; Source: TDragDockObj ect; X,Y: Integer; State: TDragState; var Accept: Boolean); В этом обработчике вы можете, как и в описанном выше, про- верить, хотите ли вы, чтобы данный оконный компонент принял перетаскиваемый компонент, определяемый параметром Source. Если принимать не надо, то задается значение Accept = false. Например, прием контейнером Panell любой панели, кроме Рапе16, обеспечивается оператором: Accept := Source.Control о Panel6; Параметр Accept будет равен true — значению по умолчанию, только если перетаскивается не PanelG. Впрочем, того же самого результата вы достигали ранее в обработчике события OnGet Si- teinfo. Объект перетаскивания Source типа TDragDockObject имеет свойство DockRect типа TRect, описывающее перамещаемую
рамку. Вы можете управлять ею. Например, если вы хотите, что- бы ваша Panels вела себя наподобие панели Microsoft Office, ко- торая может прижиматься к одной из сторон окна или перемеща- ться в середине окна в виде квадратной пенели, вы можете, уста повив предварительно в приемнике (в форме вашего приложени - или в панели Panell) DockSite равным true, написать обрабоч чик события OnDockOver в виде: if Source.Control - Panel5 then with (Sender as TWinControl).Clientorigin do with (Sender as TWinControl) do if Source.DockRect-Left<=X then Source.DockRect:=Rect(X,Y,X+25,Y+ClientHeight) else if Source.DockRect.Hight>=X+ClientWidth then Source.DockRect:=Rect (X+ClientWidth-25, Y, X+ClientWidth,Y+ClientHeight) else if Source.DockRect.Top<“Y then Source.DockRect:=Rect(X, Y,X+ClientWidth,Y+2 5) else if Source.DockRect.Bottom>=Y+ClientHeight then Source.DockRect:=Rect(X,Y+ClientHeight-25, X+ClientWidth,Y+ClientHeight) else Source.DockRect:=Rect(Source.DockRect.Left, Source.DockRect.Top, Source.DockRect.Left+100. Source.DockRect.Top+100) ; Текст написан в общем виде, пригодном и для формы, и для любого приемника. Поэтому вместо указания конкретного при- емника используется (Sender as TWinControl). Для компактно- сти использованы две конструкции with. ..do. Соответственно X и Y — вто не параметры, передаваемые в обработчик через заголо- вок процедуры, а свойства (Sender as TWinControI).CIientOri- gin, т.е. координаты левого верхнего угла клиентской области формы. A ClientWidtb и ClientHeight — это (Sender as TWin- Control). ClientWidth и (Sender as TWinControI).CIientHeight. Если в процессе буксировки панели Рапе15 ее координаты вы- ходят за пределы клиентской области приемника, то паиель вы- тягивается вдоль соответствующего края приемника, а ее шири- на равна 25. При перемещении панели внутри окна она представ- ляется квадратом со стороной 100. Это будет нормально срабатывать, если для приемника, на- пример, для панели Panell, установить UseDockManager ран- ным false. В противном случае сначала сработает диспетчер встраивания, растянув рамку клиента, а затем наступит событие OnDockOver, так что его обработчик будет иметь дело с уже из- мзненной рамкой, что приведет к ошибке встраивания. Поэтому надо отключить диспетчер встраивания для клиента Рапе15, не
отключая его для остальных клиентов. Это легко сделать, вклю- чив в обработчик события OnGetSitelnfo оператор Panel1.UseDockManager:= DockClient<>Panel5; Этот оператор задаст значение UseDockManager равным true для всех потенциальных клиентов, кроме Рапе!5. Еще одно событие, возникающее в компоненте-приемнике — OnDockDrop. Это событие возникает в момент окончания перета- скивания клиента над приемником. Оно аналогично событию OnDragDrop в технологии Drag&Drop. Заголовок соответствую- щего обработчика имеет вид: procedure TForml.PanelSDockDrop(Sender: ТОЬject; Source: TDragDockObject; X, Y: Integer); Этот обработчик позволяет разместить клиента Source тем или иным образом в зависимости от его имани, типа, местополо- жения — координат X и Y. Например, в этот момент можно что-то изменить в надписях приемника» сигнализируя пользова- теля о приеме клиента и числе клиентов. Событие, всеникающее в компоненте-приемнике в момент, когда пользователь перетаскивает клиента из приемника — OnUnDock. Заголовок соответствующего обработчика имеет вид: procedure TForml.PanellUnDock(Sender: TObject; Client: TCont rol; NewTarget: TWinControi; var Allow: Boolean); В обработчик передаются как параметры Client — компонент, который был клиентом, NewTarget — новый приемник, в кото- рый пользователь перетаскивает клиента, и Allow — параметр, определяющий, можно ли перетащить клиента. Например, если в вашем тестовом приложении вы поместите в обработчик собы- тия OnUnDock панели Panell оператор: Allow := Client о Рапе12; то, выполняя приложение, обнаружите, что Рапе12, попав клиентом в Panell, уже не может оттуда выбраться, так как ей это запрещено. Теперь, когда мы познакомились с основными возможностя- ми технологии Drag&Doc, давайте построим что-нибудь более по- леаное, чем просто игра панелями. Но сначала сохраните ваше тестовое приложение, поскольку оно еще нам потребуется. Давайте построим многооконный редактор текстов, имеющий хранилище, в которое можно помещать документы на хранение и затем извлекать из него нужные документы.
107 Начните новый проект и выполните следующие пункты. Назовите форму (свойство Name) Fmain. Разместите на форме компонент MainMenu и задайте в нем всего один пункт — Новый, подразумевая под этим созданп» нового документа. Поместите на форму панель, задайте ее свойство Caption р > ным Хранилище документов и задайте Align = alTop. Поместите на форму компонент PageControl и задайте Align alClient, чтобы он занимал вся площадь окна, кроме пол ось занятой панелью Хранилище документов (рис. 4.3 а). Задайте и нем свойство DockSite равным trne. Этот компонент будет служить приемником документов. Рис. 4.3 Формы для приложения, использующего технику Drag&Doc Добавьте в проект еще одну форму, выполнив команду File | New Form. Назовите ее FDoe (свойство Name). Установите ее свойство DragKind равным dkDock, а свойство DragMode равным dm Automatic. Эта форма будет служить клиентом компонента TPageControI на форме Fmain. Поместите на форму FDoc компонент Мето, задав для него Align = alClient, чтобы он занял всю форму (рис. 4-3 б). Со- трите в нем текст (свойство Lines). Выполните команду Project | Options ., и перенесите форму FDoc из окна Auto-create forms в окно Available forms, поскольку она должна создаваться не автоматически, а при выборе пользова- телем'раздела меню Новый (Если эта операция непонятна, см. раздел 5.1 рис. 5.1).
юа Сохраните проект, назвав модуль с первой формой Umain, а со второй — UDoc. Мы создали необходимые нам формы. Осталось написать не- сколько операторов, чтобы это все работало. Введите в раздел implementation модуля Umain операторы uses UDoc; var LDoc: TList; Первый из них дает доступ к модулю с фермой FDoc, а второй объявляет переменную LDoc типа TList. Она представляет собой список, в котором будут храниться указатели ня создаваемые по- льзователем формы документов. В обработчик события OnCreate формы Fmain запишите опе- ратор Ldoc:=TList.Create; Этот оператор создает список LDoc. Осталось написать обработчик щелчка на разделе меню Новый. Он может иметь следующий вид: procedure TForml.MNewClick(Sender: TObject); var New:TFDoc; begin New:=TFDoc.Create(Forml): LDoc-Add(New): New.Caption:='floKyMeHT 'FlntToStr(LDoc.Count); end: В этом обработчике вводится переменная New типа TFDoc, со- ответствующего типу формы FDoc. Первый оператор обработчик ка создает методом Create новую форму FDoc и присваивает ука- затель на нее переменной New. Следующий оператор добавляет указатель па эту форму в список LDoc. А последний оператор за- дает заголовок окна формы FDoc, равным Документ где много- точие заменяется на номер, соответствующий числу строк в спис- ке LDoc. Теперь ваше приложение полностью готово. Сохраните его и попробуйте в работе (рис. 4.4). При щелчке на Новый создается новая форма документа, в котором вы можете писать текст. До- кументы можно помещать в хранилище, в котором каждому до- кументу автоматически отводится новая страница. Вы можете работать с документом непосредственно в хранилище, а можете изъять его оттуда, превратив опять в отдельное окно- Для того, чтобы сделать из вашего приложения законченный продукт, нужна еще, конечно, некоторая техническая работа. Надо бы добавить разделы меню, позволяющие открыть задай-
109 Перетаскиипние объектов Рис. 4.4 Приложение, использующее технику Drag&Doc, во время выполнения ный пользователем файл и прочитать его в новый документ, по- зволяющие сохранить документ в файле, надо бы ввести при за- крытии приложения или документа запрос пользователю о необ- ходимости сохранить измененный документ и т.п. Но для того, чтобы почувствовать мощь технологии Drag&Drop, достаточно и сделанного вами примера. Причем, заметьте, что для его реали- зации вам пришлось написать всего несколько операторов. В заключение рассмотрим еще некоторые свойства и методы, управляющие встраиванием и работой с плавающими окнами. У оконных компонентов есть свойство Floating (только для чтения), которое показывает, является компонент свободно пла- вающим окном, или размещен в другом оконном компонен- те-приемнике. При переводе ранее размещенного компонента в состояние плавающего окна и при встраивании плавающего окна можно ис- пользовать свойства, в которых запоминаются размеры компо- нента в предыдущем состоянии: UndockHcighl Undock Width TBDockHeight LRDockWidth Высота компонента, которая была в последний раз, когда он отображался плавающим окном Ширина компонента, которая была в последний раз, когда он отображался плавающим окном Высота компонента, когда он в последний раз раз- мещался в контейнере вертикально Ширине компонента, когда он в последний раз раз- мещался в контейнере горизонтально Пользуясь этими свойствами можно восстанавливать при не- обходимости предшествующие размеры компонента. В частно- сти, по умолчанию после перевода компонента из размещенного
110 Глава 4 состояния в состояние плавающего окна его размеры восстанав- ливаются исходя из свойств UndockHeigbt и UndockWidth. Имеется метод ManualFIoat, который переводит размещен- ный компонент в состояние плавающего окна. Он объявлен сле- дующим образом: function ManualFIoat(ScreenFos: TRect): Boolean; Параметр функции ScreenPoe определяет положение и размер компонента после его перевода в состояние плавающего окна. Например, вы можете открыть свой тестовое приложение (рис. 4.3) и ввести в него кнопку, которая будет переводить в со- стояние плавающего окна, например, панель Рапе12. Текст обра- ботчика щелчка на такой кнопке может быть следующим: procedure TForml.ButtonlClick(Sender: TObject): var TempRect: TRect; begin TempRect.TopLeft:=Panel2-ClientToScreen(Point(0, D) ) ; TempRect.BottomRight:=₽anel2.ClientToScreen( Point (Panel.2 .UndockWidth, Panel2.UndockHeigbt)) ; Pane12.ManualFIoat(TempRect); end; В этом коде вводится переменная TempRect, в которой форми- руется прямоугольник, определяющий размер и местоположение генерируемого плавающего окна. Первые два выполняемые опе- раторы кода формируют этот прямоугольник, восстанавливая размер панели Рапе12, бывший у нее в последний раз, когда она отображалась плавающим окном. Левый верхний угол генериру- емого плавающего окна совпадает с текущим положением панели- Для правильного встраивания окна координаты с помощью мето- да ClientToScreen переводятся в координаты экрана. Последний оператор методом ManualFIoat генерирует плавающее окно. И последний метод, который мы рассмотрим — ManualDock размещающий компонент в указанном приемнике. Его описание: function ManualDock(NewDockSite: TWinControl; DropControl: TControl = nil; Controlside: TAlign = alNoneJ: Boolean; Параметр NewDockSite определяет приемник, в котором дол- жен размещаться клиент. Второй параметр DropControl опреде- ляет другой компонент в приемнике NewDockSite, который дол- жен разместить данного клиента. Как правило, этот параметр за- дается равным nil. Параметр ControISide определяет выравнива- ние клиента внутри приемника. Если это единственный клиент в приемнике, то значение этого параметра безразлично. Но если там уже есть клиенты, то задание, например, ControISide = ai-
П«у*таски«пнив объело»1 IT Left приведет к перестановке, если только данный клиент не са- мый левый в приемнике. Можете ввести в свое тестовое приложение кнопку, снабдив ее обработчиком: Panel2.ManualDock(Panell,nil,alLeft); Этот обработчик будет размещать Pnalcl2 в Panell, выравнг вая ее по левому краю приемника, если в нем расположены еп какие-нибудь клиенты. Имеется, однако, особенность, не оговоренная в документа ции: метод ManualDock срабатывает только в случае, если ком понент-клнент был ранее в состоянии плавающего окна. Так что вам придется предварительно один раз перевести Рапе12 в это со- стояние или щелчком на панели, или с помощью кнопки, соот- ветствующей ранее рассмотренному методу ManualFloat. Другой способ обойти эту трудность автоматически — ввести соответст- вующий метод ManualFloat в обработчик события формы ОпСге- ate. 4.3 Буксировка компонентов в окне приложения В ряде случаев в приложениях Windows используется переме- щение отдельных компонентов в поле окна или какой-то панели. Это перемещение может быть перемещением в какие-то заранее определенные позиции (это легко осуществляется заданием соот- ветствующих значений свойствам Left и Тор) или осуществлять- ся непрерывной буксировкой компонента с помощью мыши. Простой пример этого — буксировка компонентов по площади формы в среде разработки Delphi. Подобные задачи возникают достаточно часто в технических приложениях, связанных с ком- поновкой какого-то устройства, с формированием каких-то схем (например, электрических) и т.п. Опробовать различные способы буксировки можно в тестовом приложении, подобном приведенному на рис. 4.5. В нем на фор- ме размещено четыре компонента Image, в которые загружены какие-то изображения или части изображения (о загрузке в Ima- ge изображений и о работе с этими компонентами см. раз- дел 6.1.1). Если хотите воспроизвести пример, подобный рис. 4.5, в котором на отдельных компонентах, как на детских кубиках, воспроизведены фрагменты единого изображения, то можете в обработчик события формы OnCreate вставить следую- щий код:
iia Глава 4 Рис. 4.5 Приложение, де- монстрирующее буксировку ком- var pictzTImage, begin Pict := TImage.Create(Self}; Pict.AutoSize := true; // В следующем операторе вместо ... надо указать имя файла Pict.Picture.LoadFromFile(’ ’) ; Imagel.Canvas.CopyRect(Imagel.ClientRect, Pict.Canvas,Rect( 0,0» Pict.Width div 2, Pict.Height div 2)); Image?.Canvas-CopyRect( Image?.ClientRect, Pic t.Canvas, Rect(Pict-Width div 2,0,Pict.Width, Pict.Height div 2}); Image3.Canvas.CopyRect(Image3.ClientRect, Pict.Canvas, Rect(0,Pict.Height div 2, Pict.Width div 2,Pict.Height)); Imaged.Canvas.CopyRect(Imaged.ClientRect, Pict.Canvas, Rect(Pict.Width div 2,Pict.Height div 2, Pict.Width,Pict.Height)); Pict.Free; end; Сейчас мы не будем анализировать этот код. Он станет вам по- нятен после изучения главы 6.1.1. В компонентах Image свойст- во AutoSize должно быть установлено в true. Начнем рассмотрение на этом примере приемов буксировки. Все приведенные далее обработчики событий записаны в общем виде. Так что вы можете применять их одновременно ко всем ва- шим компонентам Image, а можете к разным компонентам при- менить разные методы, чтобы легче их было сравнивать друг с другом.
Перетаскивание объектов 113 Все методы используют несколько глобальных переменных, объявление которых надо поместить в разделе implementation модуля вне каких-либо процедур; var move: boolean ХО, ¥0:. Integer; Переменная move определяет режим буксировки. Она будет устанавливаться в true в начале буксировки и сбрасываться в fal- se в конце. Поэтому по значению move можно будет различать перемещения мыши с буксировкой и без нее. Переменные ХО и Y0 потребуются нам для запоминания координат курсора мыши. Один из возможных вариантов решения нашей задачи — бук- сировка самого компонента. Буксировка начинается при нажа- тии левой кнопки мыши на соответствующем компоненте Image. Поэтому начало определяется событием OnMouseDown, обработ- чик которого имеет вид: procedure TForml.Image1NeuseDown(Sender; TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Button О mbLeft then exit; ХО := X; Y0 := Y; move := true; (Sender as TControl).BringTcFront; end; Сначала в этой процедуре проверяется, нажата ли именно ле- вая кнопка мыши (равен ли параметр Button значению mbLeft, обозначающему левую кнопку). Затем в переменных ХО и Y0 за- поминаются координаты мыши X и Y в этот момент времени. За- дается режим буксировки — переменная move устанавливается в true. Последний оператор выдвигает методом BringToFront ком- понент, в котором произошло событие — (Sender as TControl), на передний план. Это позволит ему в дальнейшем перемещаться поверх других аналогичных компонентов. Данный оператор не обязателен, но если его не записать, то в процессе буксировки пе- ремещаемый компонент может оказаться под другими компонен- тами. Во время буксировки компонента работает его обработчик со- бытия OnMouseMove, имеющий вид: procedure TForml.ImagelMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin if move then with (Sender as TControl) do SetBounds(Left + X - X0, Top * Y - YO, Width, Height) end;
114 Глава 4 Он изменяет с помощью метода Set Bounds координаты левого верхнего угла на величину сдвига курсора мыши (X — ХО для координаты X и Y — Y0 для координаты Y). Тем самым поддер- живается постоянное расположение точки курсора в системе ко- ординат компонента, т.е. компонент смещается вслед за курсо- ром. Ширина Width и высота Height компонента остаются неиз ценными. Изменение координат можно было бы задавать непо- средственным изменением свойств Left и Тор : Left := Left + X -ХО; Тор := Тор + Y — Y0; Но поскольку эти операторы выполняются поочередно, то компонент будет делать каждый раз два перемещения: сначала по горизонтали, а потом по вертикали. Это приведет к заметному на глаз дрожанию изображения. По окончании буксировки, когда пользователь отпустит кноп- ку мыши, наступит событие OnMouseUp. Обработчик этого собы- тия должен содержать всего один оператор: move := false; указывающий приложению на окончание буксировки. Тогда при последующих событиях OnMouseMove их обработчик, привелуя- ный ранее, перестанет изменять координаты компонента. Рассмотренный вариант буксировки не свободен от недостат- ков. Основной из них — некоторое дрожание изображения при перемещении, что выглядит не очень приятно. Устранить этот недостаток позволяет другой вариант, заключающийся в буки- ровке не самого компонента, а его контура (этот вариант вы мо- жете видеть на рис. 4.5 а). При этом отмеченных выше неприят- ных явлений не наблюдается, поскольку сам компонент переме- щается только один раз — в момент окончания буксировки, ког- да требуемое положение уже выбрано. В этом варианте использу- ются методы рисования на канве, которые подробно рассматри- ваются в главе 6 в разделе 6-1- Для их применения нам потребу- ется еще одна глобальная переменная: var rec:Trect; Это объявление следует добавить к приведенным ранее объяв- лениям move, ХО и Y0. Переменная гес будет использоваться для запоминания положения перемещаемого контура компонента. Начинается процесс буксировки, как и ранее, с события On- MouseDown, которое обрабатывается процедурой вида: procedure TForml.ImagelMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShrftState; X, Y: Integer); begin
Пврстоскиипние объекте» 115 if Button о rnbLeft then exit; XO := X; YO := Y; rec := (Sender as TControl).BoundsRect; move := true; end; Эта процедура отличается от рассмотренной ранее только опе- ратором rec : = (Sender as TControl).BoundsRect; который запоминает в переменной гес исходное положение ком- понента. В процедуре отсутствует также оператор BringToFront, поскольку сам компонент не будет перемещаться и не приходит- ся заботиться о том, чтобы он оказался поверх аналогичных ком- понентов. При дальнейшем перемещении курсора мыши срабатывает об- работчик события OnMouseMove, имеющий вид: procedure TForml.ImagelMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin if not move then exit; Canvas.DrawFocusRect(rec) ; with rec do begin left := left + X - XO; right := right -Ь X'— XO; top := top + Y — YO; bottom := bottom + Y — YO; XO := X; YO := Y; Canvas.DrawFocusRect(rec); end; В этой процедуре перерисовывается и сдвигается только пря- моугольник контура компонента с помощью метода DrawFocus- Rect. Первое обращение к этому методу стирает прежнее изобра- жение контура, поскольку повторная прорисовка того же изобра- жения по операции ИЛИ (ог) стирает нанесенное ранее изображе- ние (см. главу 6, раздел 6.1.5). Затем изменяются значения, хра- нимые в переменной гес, и той же функцией DrawFocusRect осу- ществляется прорисовка сдвинутого прямоугольника. При этом сам компонент оствется на месте. Когда пользователь отпускает кнопку мыши, наступает собы- тие OnMouseUp и срабатывает следующая процедура: procedure TForml.ImagelMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: integer);
ИД Глоио 4 begin Canvas.DrawFocusRect(rec); with (Sender as TControl) do begin SetBounds(rec.Left + X — XO, rec.Top + Y — YO, Width, Height); BringToFront; end; move := false; end; Первый ее оператор стирает последнее изображение контура, а второй оператор перемещает компонент в новую позицию.-Опе- ратор BringToFront не является обязательным. Он проявит себя только в случае, если перемещенный компонент ляжет поверх другого аналогичного компонента. Приведенный алгоритм исключает мерцание изображения, так как само перемещение компонента осуществляется только в момент отпускания кнопки мыши. К тому же при этом в обработ- чике события OnMouseUp легко предусмотреть какие-то условия отказа от перемещения (например, нажатую клавишу Alt). Для этого достаточно в приведенный выше обработчик добавить пе- ред оператором with оператор: if not (ssAlt rn Shift) then В этом случае, если пользователь перед завершением букси- ровки нажмет клавишу Alt, компонент не переместится. Это по- лезно в некоторых приложениях, связанных с каким-то проекти- рованием топологии или размещения предметов: прикинув новое положение компонента пользователь может тут же вернуться к предыдущему, если новое расположение неудачно. С другой сто- роны, для некоторых задач буксировка только контура мало ин- формативна. Например, при проектировании какой-то мозаики перемещение контура не даст представления о том, подойдет ли компонент по своему виду к данному месту. В этих случаях пред- почтительнее буксировка всего изображения. Описанный выше способ перемещения изображения контура компонента можно осуществить иначе с использованием техники Drag&Dock и методов ManualFloat и ManualDock, описанных в разделе 4.2. Чтобы опробовать этот подход, установите в форме свойство DockSite равным true и сделайте в перемещаемых ком- понентах (в нашем примере — в Image) установку свойств Drag- Kind в dkDock и DragMode в dm Automatic Это надо сделать по крайней мере в двух компонентах. Если перемещаемым будет то- лько один компонент, описываемый метод не сработает. В начале работы приложения надо выполнить для каждого компонента, который в дальнейшем может перемещаться, методы ManualFlo-
Перетоскиионие объектен 117 at и Manual!) ock, назначив приемником в Manual Dock форму. Для этого можно вставить в обработчик события формы ОпСгеа- te, например, следующие операторы: Image1.Man* alFloat(Rect(Forml.LefttImagel.Left, Forml.Top+Tmagel.Top, Forml.Left+Imagel.Left+Imagel.Width, Forml.Top+Image1.Top-Hmagel .Height)}/ Imagel.ManualDock(Forml,nil,alLeft); Аналогичные операторы с заменой имени компонента Imagel надо вставить и для других перемещаемых компонентов. И это все. Вам не надо писать никаких обработчиков событий компонентов. Вы получили приложение, в котором пользователь может перемещать мышью ваши изображения. При этом в про- цессе буксировки перемещается только контур изображения в виде утолщенной рамки (рис. 4.5 б). Сам компонент перемещает- ся на новое место только в конце буксировки. Если хотите, то мо- жете в обработчик события OnEmLDock внести оператор (Sender as TControl)-BringToFront; который обеспечит в момент переноса размещение компонента поверх других.

Глава 5 Формы 5.1 Управление формами Форма — основа любого приложения. Обычно сколько-нибудь сложное приложение содержит несколько форы. Включение в проект новой формы осуществляется командой File | New Form или командой File | New с последующим выбором формы из Депозита- рия. По умолчанию все формы создаются автоматически при за- пуске приложения и первая из наеденных в приложение форм считается главной. Главная форма отличается от прочих рядом свойств. Во-первых, именно этой форме передается управление в начале выполнения приложения. Во-вторых, закрытие пользова- телем главной формы означает завершение выполнения прило- жения. В-третьих, главная форма так же, как и любая другая, может быть спроектирована невидимой, но если все остальные формы закрыты, то главная форма становится в любом случае видимой (иначе пользователь не смог бы продолжать работать с приложением и даже не смог бы его завершить). Указанные выше условия, принятые по умолчанию (первая форма —- главная, все формы создаются автоматически), могут быть изменены. Главной в вашем приложении может быть вовсе не та форма, которая была спроектирована первой. Не стоит твк- же в общем случае все формы делать создаваемыми автоматиче- ски. В приложении могут быть предусмотрены формы (например, формы для установки различных опций), которые требуются да- леко не в каждом сеансе работы с приложением. Было бы варвар- ским расточительством создавать на всякий случай такие формы автоматически при каждом запуске приложения и занимать под них память. А в приложениях MDI дочерние формы в принципе не могут быть автоматически создаваемыми, так как число таких форм определяет пользователь во время работы приложения, со- здавая каждую новую форму командой типа Новое окно. Изменить принятые по умолчанию условия относительно форм можно в окне Опций проекта, которое вызывается, напри- мер, командой Project | Options в Delphi 3 и 4 или командой Options ] Project в Delphi 1. В открывшемся окне Опций проекта (Project Options) надо выбрать страницу Forms, представленную на рис. 5.1. В верхнем выпадающем списке Main forms можно выбрать главную форму среди имеющихся в проекте. Пользуясь двумя
120 Рис. 5.1 Строница Forms окна Опций проекта нижними окнами можно установить, какие формы должны со- здаваться автоматически, а какие не должны. Например, если надо исключить форму Form2 из списка автоматически создавае- мых, то надо выделить ее в левом окне (Auto-creote forms) и с помо- щью кнопки со стрелкой, направленной вправо, переместить в правое окно доступных форм (Available forms). Для каждой автоматически создаваемой формы Delphi добав- ляет в головной файл проекта соответствующий оператор ее со- здания методом CreateForrn Это можно увидеть, если выпол- нить команду Project | View Source и просмотреть появившийся файл проекта .dpr. Он может, например, содержать следующие выполняемые операторы: Application.CreateForrn(TForml, Forml); Application.CreateForm(TAboutBox, AboutBox); Application.Run; Первый и второй из них создают соответствующие формы, а третий начинает выполнение приложения. Для форм, которые были исключены из списка автоматически создаваемых, аналогичный метод CreateForrn надо выполнить в тот момент, когда форма должна быть создана. В момент создания формы возникает событие OnCreate. Обра- ботка этого события широко используется для настройки ка- ких-то компонентов формы, создания списков и т.д. В нужный момент форму можно сделать видимой методами Show или ShowModal. Последний метод открывает форму как модальную. Это означает, что управление передается этой форме
Формы 121 и пользователь не может передать фокус другой форме данного приложения до тех пор, пока он не закроет модальную форму. Более подробное описание модальных форм и примеры работы с ними будут рассмотрены позднее в разделах 5.2 и 5-3. Методы Show и ShowModal можно применять только к неви- димой в данный момент формы. Если нет уверенности, что форма в данный момент видима, то прежде, чем применять эти методы, следует проверить свойство Visible формы. Например: if (not Form2.Visible) then Form2.ShowModal;; При выполнении методов Show или ShowModal возникает со- бытие формы onShow. Это событие возникает до того момента, как форма действительно станет видимой. Поэтому обработку со- бытия onShow можно использовать для настройки каких-то ком- понентов открываемой формы. Отличие от упомянутой ранее на- стройки компонентов в момент события onCreate заключается в том, что событие onCreate наступает для каждой формы только один раз в момент ее создания, а события onShow наступают каждый раз, когда форма делается видимой. Так что при этом в настройке можно использовать какую-то оперативную информа- цию, возникающую в процессе выполнения приложения. Методом Hide форму в любой момент можно сделать невиди- мой. В этот момент в ней возникает событие onHide. Необходимо помнить, что для выполнения методов Create- Form, Show, ShowModal, Hide и вообще для обмена любой ин- формацией между формами модули соответствующих форм дол- жны использовать друг друга. Например, если форма в модуле Unitl должна управлять формой в модуле Unit2, то в оператор uses модуля Unitl должно быть включено имя второго модуля Unit2. А если к тому же форма в модуле Unit2 должна пользова- ться какой-то информацией, содержащейся в модуле Unitl, то в оператор uses модуля Unit2 должно быть включено имя первого модуля Unitl. В этом случае, если операторы uses в обоих моду- лях расположены в разделах interface, возникнут проблемы с не- допустимыми круговыми ссылками (circular unit reference) и компи- лятор выдаст соответствующую ошибку. От недопустимых круго- вых ссылок можно избавиться, если разомкнуть их, поместив один или оба оператора uses в раздел implementation. Впрочем, проще не включать имена модулей приложения в операторы uses вручную, а использовать команду File ] Use Unit, которая автомати- зирует этот процесс и гарантирует отсутствие круговых ссылок. Закрыть форму можно методом Close. При этом в закрываю- щейся форме возникает последовательность событий, которые можно обрабатывать. Их назначение — проверить возможность
122 закрытия формы и указать, что именно подразумевается под за- крытием формы. Проверка возможности закрытия формы необ- ходима, например, для того, чтобы проанализировать, сохранил ли пользователь документ, с которым он работал в данной форме и который изменял. Если не сохранил, приложение должно спро- сить его о необходимости сохранения и, в зависимости от ответа пользователя, сохранить документ, закрыть приложение без со- хранения или вообще отменить закрытие. Рассмотрим последовательность событий, возникающих при выполнении метода Close- Первым возникает событие onCloseQiiery. В его обработчик передается как var (по ссылке) булева переменная CanClose, определяющая, должно ли продолжаться закрытие формы. По умолчанию CanClose равно true, что означает продолжение за- крытия. Но если из анализа текущего состояния приложения или из ответа пользователя на запрос о закрытии формы следует, что закрывать ее не надо, параметру CanClose должно быть при- своено значение false. Тогда последующих событий, связанных с закрытием формы не будет. Например, пусть в приложении имеется окно редактирования Memol, в котором свойство Modified указывает на то, был ли из- менен пользователем текст в этом окне с момента его последнего сохранения. Тогда обработчик события onCloseQuery может иметь вид: procedure TForml.FormClcseQuerу(Sender: TObject; var CanClose: Boolean); begin if (Application.MessageBox(’Текст документа не сохранен.* + ' Действительно хотите закончить работу ?’, •Подтвердите завершение работы’, MB_ICONQUESTION+MB_YESNOCANCEL) <> IDYES) then CanClose:=false; end; В приведенном операторе вызывается методом Applicati- on.MessageBox диалоговое окно. Первый параметр метода содер- жит текст сообщения пользователю. Второй параметр — текст заголовка окна. Третий параметр — набор флагов, определяю- щих вид окна- Флаг MB_ICONQUESTION отображает в окне пиктограмму вопросительного знака- Флаг MB_YESNOCANCEL заносит в окно кнопки До, Нет и Отмена. В результате пользова- телю будет показано окно, приведенное на рис. 5.2. Функция MessageBox возвращает результат, который указы- вает на реакцию пользователя в диалоговом окне. Значение
123 Рис. S.2 Дирлотеое окно, вызываемое методом Applicoiion MessogeBox IDYES возвращается, если пользователь нажал кнопку До, зна- чение IDNO возвращается при нажатии кнопки Нет, значение IDCANCEL — при нажатии кнопки Отмена или клавиши Esc. По- дробнее и методе Application.MessageBox вы можете узнать во встроенной справке Delphi. В нашем примере, если пользователь в диалоговом окне с за- просом о сохранении не ответит До, то CanClose будет равно false и окно не закроется. Причем этот обработчик сработает при лю- бой попытке пользователя закрыть приложение: нажатии в нем кнопки или раздела меню Выход, нажатии кнопки системного меню в полосе заголовка окна и т.п. Если обработчик событня onCloseQuery отсутствует или если в его обработчике сохранено значение true параметра CanClose, то следом наступает событие OnClose. В обработчик этого собы- тия передается как var переменная Action, которой можно зада- вать значения: caNone Не закрывать форму. Это позволяет и в обработчике данного события еще отказаться от закрытия формы caHide При этом значении (оно принято по умолчанию для форм, не являющихся главными и дочерними в прило- жениях MDI) закрыть форму будет означать сделать ее невидимой. Для пользователя она исчезнет с экрана, од- нако вся хранящаяся в форме информация сохранится. Она может использоваться другими формами или той же самой формой, если она снова будет сделана видимой caMinimize При этом значении закрыть форму будет означать свер- caFree нуть ее до пиктограммы. Как и в предыдущем случае, вся информация в форме будет сохранена При этом значении закрыть форму будет означать унич- тожение формы и освобождение занимаемой ею памяти. Вся информация, содержащаяся в форме, будет уничто- жена. Если эта форма в дальнейшем потребуется еще раз, ее надо будет создавать методом CreateForrn Не все значения Action допустимы для любых форм. Напри- мер, для дочерних форм в приложении MDI возможны значения только caNone и caMinimize
124 Если в обработчике события OnClose задано значение Action, равное caFree, то при освобождении памяти возникает еще одно последнее событие — OnDestroy. Оно обычно используется для очистки памяти от тех объектов, которые автоматически не уничтожаются при закрытии приложения. Начиная с Delphi 4 формы имеют свойство OldCreateOrder, определяющее моменты событий OnCreate и OnDestroy. Если это свойство установлено в false (значение по умолчанию), то собы- тие OnCreate наступает после того, как закончили работу все конструкторы компонентов, содержащихся на форме, а событие OnDestroy наступает прежде, чем вызывается какой-либо дест- руктор. При OldCreateOrder = true, что соответствует поведению компонентов в Delphi 3 и более ранних версиях, событие ОпСгев- te наступает при выполнении конструктора TCustomForm, а со- бытие OnDestroy наступает при выполнении деструктора ТСив- tomForm. 5. 2 Модальные формы Открытие форм как модальных используется в большинстве диалоговых окон. Модальная форма приостанавливает выполне- ние вызвавшей ее процедуры до тех пор, пока пользователь не за- кроет эту форму. Модальная форма не позволяет также пользова- телю переключить фокус курсором мыши на другие формы дан- ного приложения, пока форма не будет закрыта. Т.е. пользова- тель должен выполнить предложенные ему действия прежде, чем продолжить работу. Модальной может быть сделана любая форма, если она делает- ся видимой методом ShowModal. Если тв же самая форма делает- ся видимой методом Show, то она не будет модальной. Поведение модальной формы определяется ее основным свой- ством ModalResult. Это свойство доступно только во время вы- полнения приложения. При открытии формы методом ShowMo- dal сначала свойство ModalResult равно нулю. Как только при обработке каких-то событий на форме свойству ModalResult бу- дет присвоено положительное значение от 1 до 8, модальная фор- ма закроется. А значение ее свойства ModalResult можно будет прочитать как результат, возвращаемый методом ShowModal Таким образом программа, вызвавшая модальную форму, может узнать, что сделал пользователь, работая с этой формой, напри- мер, на какой кнопке он щелкнул.
Формы 125 В Delphi предопределены некоторые константы, облегчающие трактовку результатов, полученных при закрытии модальной формы: Численное значе- ние ModalResult Константа Пояснение 0 inrNone 1 mrOk idOK Закрытие модальной формы нажатием кнопки ОК 2 mrCancel или idCanosl кнопки Cancel, или методом Close, или нажатием кнопки системного меню в полосе заголовка окне 3 mr Abort или idAbort Закрытие модальной формы нажатием кнопки Abort 4 mrRetry idRetry Закрытие модальной формы нажатием кнопки Retry 5 mrlgnore или idlgnore Закрытие модальной формы нажатием кнопки ignore 6 mrYes или idYes Закрытие модальной формы нажатием кнопки Yes 7 mrNo или idNo Закрытие модальной формы нажатием кнопки No 8 inrAll Закрытие модальной формы нажатием кнопки АП (только начиная с Delphi 3) Все приведенные выше пояснения значений ModalResult (кроме значений О и 2) носят чисто условный характер. В своем приложении вы вольны трактовать ту или иную величину Mo- dalResult и соответствующие константы как вам угодно. Требуемые значения ModalResult можно задавать в обработ- чиках соответствующих событий в компонентах модальной фор- мы. Однако при использовании кнопок можно обойтись и без по- добных обработчиков. Дело в том, что кнопки типа TButton и TBitBtn имеют свойство ModalResult, по умолчанию равное mrNone. Для кнопок, расположенных на модальной форме, зна-
126 Гло«а 5 чение этого свойства можно изменить и тогда не потребуется вво- дить каких-либо обработчиков событий при щелчке на них. В кнопках BitBtn при свойстве Kind, не равном bkCustom, заложе- ны по умолчанию значения ModalResult, соответствующие на- значению той или иной кнопки. Ниже приведен пример использования модальных форм. 5. 3 Пример приложения с модальными формами заставки и запроса пароля Во многих больших приложениях при их запуске сначала на экране появляется форма-заставка, содержащая логотип прило- жения и сведения о программе и ее разработчике. Назначение этой формы чаще всего заключается в том, чтобы обеспечить на- чальную загрузку и настройку программы. Тогда эта форма дол- жна закрываться не раньше, чем закончатся эти операции. Но иногда эта форма носит чисто информационный характер. В этих случаях желательно, чтобы она немедленно закрывалась при лю- бых действиях пользователя и даже закрывалась через какое-то время без каких-либо действий со стороны пользователя. Именно такую форму-заставку мы и попробуем создать. Помимо формы-заставки нередко в приложениях, особенно в тех, которые работают с базами данных, в начале работы прило- жения появляется форма с запросом пароля. При неверном паро- ле приложение закрывается, не позволяя пользователю работать с ним. Формы-заставки и формы запроса пароля могут быть реализо- ваны множеством различных способов. Рассмотрим один из них. 1. Откройте в Delphi новое приложение (Fite | New Application). Пусть открывшаяся форма будет главной в нашем приложе- нии (вместо такой пустой формы вы можете взять любое раз- работанное вами ранее приложение и добавлять форму-застав- ку и форму запроса пароля в него). Назовите для определенно- сти главную форму приложения FMain. 2. Добавьте в приложение новую форму (File | New Form). Пусть это будет ваша форма-заставка. Назовите ее FLog. Ее свойство BorderStyle надо сделать равным bsNone (см. раздел 1.3), что- бы в окна этой формы отсутствовала полоса заголовка. Вы мо- жете поместить на форме какой-то рисунок (разместить ком- понент Image и вставить в его свойстве Picture желаемое изоб- ражение), надписи и т.п. В простейшем случае поместите в центре формы метку Label и напишите в ней какой-то текст.
Формы 127 Размер формы-заставки задайте небольшим, меньшим, чем обычные окна приложения. Свойство Position следует сделать равным poScreenCenter, чтобы форма появлялась в центре ак- рана. 3. Теперь напишите обработчики событий, которые при любом действии пользователя закрывали бы форму. Щелкните на форме, чтобы в Инспекторе Объектов открылись относящиеся к ней страницы (если у вас форма накрыта панелями или ри- сунками, то, щелкнув на них, нажимайте клавишу Esc до тех пор, пока в Инспекторе Объектов не откроются страницы» от- носящиеся к форме). Перейдите в Инспекторе Объектов на страницы событий, выберите событие onKeyDown и напишите для него обработчик, состоящий из одного оператора — Close. Аналогичный обработчик напишите для события onMouse- Down. Если на форме у вас имеются метки, компоненты Ima- ge и др., то выделите их все, задайте в событии onMouseDown ссылку на тот же обработчик, что вы сделали для формы, а в форме поставьте свойство KeyPreview в tine, чтобы форма пе- рехватывала все связанные с нажатием клавиш события ком- понентов. Теперь форма будет закрываться при нажатии пользователем любой клавиши или кнопки мыши. Давайте сделаем так, чтобы и при отсутствии каких-то действий со стороны пользователя форма закрывалась сама, например, через 5 секунд. 4. Добавьте на форму компонент Timer со страницы System. Это невизуальный компонент, который может отсчитывать интер- валы времени. Интервал задается в свойстве компонента In- terval в миллисекундах. Задайте его равным 5000. Единствен- ное событие таймера — OnTimer, наступающее по истечении заданного интервала времени. Напишите в обработчике этого события все тот же единственный оперетор Close. Теперь при любом действии и даже бездействии пользователя форма-заставка будет закрываться. Но что это означает? По умолчанию закрыть форму значит сделать ее невидимой. Одна- ко, форме-заставка не нужна после того, как она будет предъяв- лена пользователю в первый момент выполнения приложения. Хранить все время в памяти эту уже ненужную форму не имеет смысла. Поэтому в форме надо предусмотреть, чтобы закрытие формы означало ее удаление из памяти и освобождение памяти для чего-нибудь более полезного. Для этого надо сделать следую- щее. 5. В событие формы OnClose вставьте оператор:
12а Action:=caFree; Как указывалось в предыдущих разделах, этот оператор при- водит к уничтожению объекта формы и освобождению занимае- мой формой памяти. Форма-заставка готова к использованию. Проверьте только, имеет ли ее свойство Visible значение false. Это важно, посколь- ку только невидимую форму можно открыть методом ShowMo- dal. В главной форме свойство Visible тоже должно иметь значе- ние false. Сохраните проект, дав файлу модуля главной формы имя Umain, а файлу модуля формы-заставки имя FLog. Теперь можно записать в главной форме оператор ShowModal. Но чтобы это можно было сделать, необходимо сослаться в моду- ле Umain на модуль FLog- 6. Добавьте в оператор uses модуля Umain имя модуля FLog, или напишите оператор nses FLog в разделе implementation модуля Umain, или сделайте то же свмое путем выполнения команды File | Use Unit. Впрочем, если вы забудете это сделать, Delphi сама поправит вас и добавит этот оператор при попытке компиляции приложе- ния, задав вопрос: «Form 'Fmoin' references form ’FLog’ declared in unit ’ULog' which is not in your USES list. Do you wish to odd it?», что означает «Форма ’Fmoin' ссылается на форму 'Flog', объявленную в модуле 'ULog', который отсутствует в вашем списке USES. Хотите ли вы добавить его в список?». При положительном ответе на этот вопрос соответству- ющий оператор будет добавлен в ваш модуль Umain. После этого осталось написать в модуле Umain обработчик со- бытия формы OnShow. 7. Напишите в модуле Umain обработчик события формы OnS- how, состоящий из одного оператора: FLog.ShowModal; Событие OnShow наступает до того, как форма действительно станет видимой. Поэтому во время обработки этого события глав- ная форма приложения еще не видна. Оператор открывает форму FLog как модальную, передает ей управление и дальнейшее вы- полнение программы в модуле Umain останавливается до тех пор, пока модальная форма не будет закрыта. После закрытия модальной формы выполнение программы продолжится и глав- ная форма станет видимой. Можете сохранить проект, запустить приложение и убедить- ся, что все работает правильно. Теперь добавим в приложение форму запроса пароля. Реаль- ная форма такого типа должна предлагать пользователю ввести
129 свое имя и пароль, сравнивать введенные значения с образцами, хранящимися где-то в системе, при неправильном пароле давать возможность пользователю поправиться. Если пользователь так и не может ввести правильный пароль, форма должна закрыть приложение, не допустив к нему пользователя. При правильном пароле после закрытия формы запроса должна открыться глав- ная форма приложения. Все это не трудно сделать, но мы упрос- тим задачу, чтобы не отвлекаться от главного — взаимодействия форм в приложении. Будем использовать всего один пароль, ко- торый непосредственно укажем в соответствующем операторе программы. И не будем давать пользователю возможности испра- вить введенный пароль. 8. Добавьте к приложению новую форму. Назовите ее FPSW и сохраните ее модуль в файле с именем UPSW. Уменьшите размер формы до разумных пределов, поскольку она будет со- держать всего одно окно редактирования. Установите свойст- во формы BorderStyle равным bsDialog. свойство Position равным poScreenCenter. В свойстве Caption напишите «Введи- те пороль и ножмите Enter». Эта надпись будет служить пригла- шением пользователю. 9. Поместите в центре формы окно редактирования Edit, в кото- ром пользователь будет вводить пароль. Очистите его свойство Text. В обработчике события OnKeyDown этого компонента запишите оператор: if {key = VK_RETURN) then begin if EPSW.Text - ‘a’ then ModalKesult := 6 else Close; end; Этот оператор выполняет следующее. Прежде всего он анали- зирует нажатую клавишу (подробнее об этом смотрите в разделе 3.2.2). Если нажата клавиша Enter, то введенный текст сличается с паролем. В данном операторе для упрощения непосредственно указан правильный пароль — символ 'o’. Если введен правиль- ный пароль, то свойству ModalResult присваивается некоторое условное число — 6 (можно было бы выбрать и любое другое до- пустимое число, кроме О и 2). Если пароль неправильный, то вы- полняется метод Close. В обоих случаях форма закрывается, так как задание отличного от нуля положительного значения Modal- Result равносильно закрытию формы. Но при правильном паро- ле значанне ModalResult будет равно б, а при неправильном — 2. Это значение получается при выполнении метода Close или если
130 Глсы» 5 пользователь нажмет кнопку системного меню (кнопку с крести- ком) в полосе заголовка окна. На этом проектирование формы запроса пароля закончено. Те- перь запишем в модуле главной формы Uinain оператор, показы- вающий пользователю эту форму и анализирующий ответ поль- зователя. Для этого надо выполнить следующие операции. 10.В модуле Umain, надо, как и ранее, добавить в оператор uses ссылку на модуль UPSW, а в обработчике события OnShow после ранее введенного оператора FLog.ShowModal добавить оператор: if (FPSW.-ShowModal О 6) then Close else begin ShowMessage (1 Ваш пароль • ’ +FPSW.EPSW. Text*1 1 " ) ; FPSW.Free; Этот оператор анализирует значение свойства ModalResult формы запроса пароля. Значение этого свойства возвращает фун- кция FPSW.ShowModal. Если результат не равен 6. то был вве- ден неправильный пароль. Тогда главная форма, а с пей вместе и приложение, закрываются методом Close. При правильном паро- ле можно продолжать работу приложения. Оператор SbowMcssa- ge введен просто для того, чтобы показать, как можно использо- вать свойство другой формы — в данном случае текст, введенный пользователем в качестве пароли. В реальном приложении по этому паролю можно было бы определить уровень доступа поль- зователя к конфиденциальной информации. Затем следует унич- тожение формы запроса пароля методом Free. Это необходимо сделать, чтобы освободить память. Сама по себе эта форма в мо- мент ее закрытия не уничтожается, поскольку по умолчанию за- крыть форму — значит сделать ее невидимой. Уничтожать фор- му до этого момента было нельзя, так как при этом уничтожи- лась бы содержащаяся в пей информация — введенный пароль. На этом разработка нашего приложения закончена. Можете сохранить проект, запустить приложение и посмотреть, как оно работает. Описанный выше способ управления формой запроса пароля не является оптимальным. Он просто призван был показать, как можно обрабатывать величину ModalResult, возвращаемую ме- тодом Show Modal. Но то же самое можно было бы сделать и про- ще. В обработчике события OnKeyDown окна редактирования на форме FPSW можно было бы написать более простой оператор:
Формы 131 if (key = VK_RETURN) and (EPSW.Iext о ’a’) then Application.Terminate; При неверном пароле этот оператор завершает работу всего приложения методом AppLication.Tenninate. Тогда в главной форме не надо анализировать результат работы пользователя с формой FPSW, так как если приложение не закрылось при вы- полнении оператора ShowModal, то значит пароль введен прави- льный. Поэтому операторы в главной форме тоже упрощаются: FPSW.ShowModal; ShowMessage('Вам пароль 1’’+FPSW.EPSW.Text+*'’'); FPSW.Free; Проведите в вашем приложении соответствующие замены операторов и убедитесь, что приложение и в этом случае работает правильно. 5.4 Управление формами в приложениях с интерфейсом множества документов (приложениях MDI) Типичным приложением MDI является привычный всем Word. В приложении MDI имеется родительская (первичная) форма и ряд дочерних форм (называемых также формами доку- ментов). Окна документов могут создаваться самим пользовате- лем в процессе выполнения приложения с помощью команд типа Окно | Новое. Число дочерних окон заранее неизвестно — пользо- ватель может создать их столько, сколько ему потребуется. Окна документов располагаются в клиентской области родительской формы. Поэтому чаще всего целесообразно в родительской форме ограничиваться только главным меню, инструментальными па- нелями и, если необходимо, панелью состояния, оставляя все остальное место в окне для окон дочерних форм. При этом обыч- но окно родительской формы в исходном состоянии разворачива- ют на весь экран. Из родительской формы можно управлять дочерними форма- ми. Дочернюю форму нельзя уничтожить пока не уничтожена ро- дительская форма. Требования, которые надо учитывать при разработке прило- жений MDI, рассмотрены в разделе 1.2. Для создания приложения MDI необходимо спроектировать родительскую и дочернюю формы. В родительской форме свойст- во Form Style устанавливается в fsMDIForm, а в дочерней — в fsMDIChild. Поскольку дочерние окна будет создавать сам поль-
132 Глава 5 зователь в процессе выполнения приложения, дочернюю форму необходимо исключить из числа создаваемых автоматически (в разделе 5.1 рассказывалось, как это сделать с помощью окна Оп- ций проекта, представленного на рис. 5-1). Рассмотрим теперь, как можно сделать обработчик команды, по которой пользователь в родительском окне задает создание но- вого окна документов — нового экземпляра дочерней формы. Этот обработчик может иметь вид: Var <имя> : «имя класса дочерней формы>; begin «имя>:=«имя класса дочерней формы>.Сгeate(Application); «операторы настройки, если они нужНы> <имя>.Show; end; Переменная, объявленная в этой процедуре, используется для создания произвольного временного имени (указателя) вновь со- здаваемого объекта — формы. Первый из выполняемых операто- ров процедуры создает этот объект. Далее могут следовать ка- кие-то операторы настройки нового дочернего окна. Например, новому окну надо присвоить какой-то уникальный заголовок (свойство Caption дочерней формы), чтобы пользователь мог от- личать друг от друга окна документов — это безусловное требова- ние к приложениям MDI Windows. Последний оператор процеду- ры делает видимым вновь созданное окно. Пусть, например, вы создали в модуле UMain родительскую форму, содержащую раздел меню Окно | Новое, и создали в моду- ле UDoc дочернюю форму с именем FDoc, имеющую тип TFDoc (посмотреть для контроля имя и тип дочерней формы вы можете в верхнем выпадающем списке Инспектора Объектов, выделив интересующую вас форму, или в модуле, посмотрев автоматиче- ски создаваемый Delphi оператор, объявляющий переменную формы и расположенный сразу после определения класса данной формы). Тогда в операторе uses модуля родительской формы вы дол- жны сослаться на модуль дочерней формы UDoc. А в обработчи- ке события, связанного с выбором пользователем раздела меню Окно | Новое, можно написать обработчик вида: var NewF : TFDoc; begin NewF := TFDoc.Create(Application); NewF.Show; end;
Формы 133 В родительской форме имеется ряд свойств, позволяющих управлять дочерними окнами. Все они доступны только для чте- ния и только во время выполнения. Свойство MDIChildCount определяет количество открытых дочерних окон. Свойство MDIChildren[i;integer] дает доступ к i-му окну (окна индексируются в порядке их создания). Приве- дем оператор, который можно вставить в предыдущий пример для задания уникального имени вновь созданного окна NewF: NewF.Caption :=' 'Окно + IntloStr(MDIChildCount); Следующий пример показывает процедуру, с помощью кото- рой из родительской формы Forml можно закрыть (свернуть) все дочерние окна, начиная с последнего: var I: Integer; begin with Forml do for 1 := MDIChildCount-1 downto 0 do MDIChildren[I].Close; end; В момент создания окон документов они автоматически распо- лагаются каскадом в клиентской области родительской формы. При этом, если размера клиентской области не хватает для раз- мещения дочерних окон, размеры последних автоматически уме- ньшаются. Имеется ряд методов родительской формы, упорядо- чивающих размещение дочерних окон. Метод Cascade располага- ет все открытые (не свернутые) окна каскадом. Метод Tile распо- лагает окна мозаикой. При этом учитывается свойство родитель- ской формы TileMode. Если око равно tbVertical, то упорядочи- вание производится по вертикали, а если TileMode равно tbllori- zontal, то упорядочивание производится по горизонтали. Метод Arrangelcons упорядочивает расположение пиктограмм сверну- тых окон. Отдельно надо упомянуть об объединении главных меню роди- тельской и дочерних форм. Обычно обе эти формы имеют глав- ные меню, но они различны. Например, родительская форма мо- жет иметь меню Окно, а дочерняя форма — меню Фойл и Правка. Меню дочерних форм не должно появляться в окнах документов, а должно всегда встраиваться в главное меню родительской фор- мы. Поэтому свойство AutoMerge компонента типа TMainMenu на приложения MDI не влияет: встраивание меню происходит не- зависимо от значения этого свойства. А места, на которые встра- иваются разделы меню дочерней формы, определяются значени- ями свойства Grouplndex каждого раздела меню. Например, если вы хотите, чтобы после объединения меню родительской и
134 Глава 5 дочерней форм на первом месте было меню дочерней формы Фойл, на втором — Провка, а на третьем — меню родительской формы Окно, то во всех разделах меню Окно надо задать Groupln- dex = 2, во всех разделах меню Фойл — Grouplndex = 0, во всех разделах меню Правка — Grouplndex = 1. Тогда объединение меню произойдет в нужной нам последоватальности. 5.5 Пример приложения с интерфейсом множества документов — простой многооконный редактор Рассмотрим проектирование простого приложения MDI. Пусть мы хотим создать многооконный редактор, в каждое окно которого можно загрузить содержимое заданного входного тек- стового файла, что-то в нем изменить и сохранить текст в задан- ном выходном файле (рис. 5.3). Для простоты построим дочерние окна с использованием компонентов Мето и не будем тратить время на разработку сервиса, необходимого в реальных редакто- рах. Рис. 5-3 Пример приложения MDI- многооконный редоктор в режимах упорядочивания окон каскадом (о) и по горизонтали (б)
Формы 135 Начнем с построения формы окна документа. 1. Откройте в Delphi новое приложение. 2. Назовите открывшуюся форму FDoc. 3. Разместите на форме компонент Memol типа ТМепю. Его свойство Align задайте равным aJClient, чтобы окно редакти- рования заняло всю площадь окна. Сотрите текст, появив- шийся в Memol (свойство Lines). 4. Разместите на форме по одному компоненту типов TOpenDia- log и TSaveDialog. Задайте в обоих диалогах свойства DefanL tExt равными txt, а в свойства Filter занесите строку «тексто- вые файлы» с шаблоном *.fxt и строку «все фойлы» с шаблоном 5. Разместите на форме компонент типа MainMenu. Создайте в нем раздел Фойл (присвойте его свойству Name значение MFi- 1е) с подразделами Открыть... (Name МОреп)и Сохранить как.. (Name = MSave). 6. Теперь напишите обработчики событий для введенных по- дразделов меню. Для раздела МОреп обработчик может иметь вид: if OpenDialog1.Execute then begin Memol.Clear; Memol.Lines _ LoadFromFile(OpenDialogl.Filename); end; Для раздела MSave обработчик может иметь вид: if SaveDialogl.Execute then Memol.Lines.SaveToFile(SaveDialogl.Filename); 7. Сохраните проект, дав спроектированному модулю имя UDoc. Простенький редактор (пока однооконный) построен. Можете скомпилировать его и проверить в работе- Теперь давайте спроектируем родительскую форму и превра- тим наш однооконный редактор в многооконный. 8. Измените свойство FormStyle созданной вами ранее формы на fsMDIChild. 9. Откройте в вашем проекта новую форму (File | New Form). Назо- вите ее FMDI. Сохраните ее модуль с именем UMDI (File | Save As..). 10. Измените свойство FormStyle новой формы на fsMDIForm. Задайте свойство Windows tat е равным wsMaximized, чтобы окно этой формы предъявлялось пользователю в первый мо-
136 Глово 5 мент развернутым на весь экран. В оператор nses модуля UMDI введите ссылку на модуль дочерней формы UDoc (иначе вы не сможете открывать дочерние формы и управлять ими). 11. Выполните команду Project [ Options и в открывшемся окне на странице Forms (см. рис. 5.1) переведите дочернюю форму FDoc из списка автоматически создаваемых в список доступ- ных. При этом, как вы сможете убедиться по выпадающему списку вверху окна, главной станет родительская форма UMDI — единственная, оставшаяся в списке автоматически создаваемых форм. 12. Введите на форму UMDI компонент типа MainMenu. Сформи- руйте в нем раздел меню Окно (имя Name = MWind) с подраз- делами Новое (Name = MNew), Каскад (Name = MCascade), Упорядочить По горизонтали (Name ~ МНог), Упорядочить по вер- тикали (Name = MVert), Упорядочить значки (Name = MIcons). Чтобы введенные разделы меню не заменялись во время вы- полнения разделами меню дочернего окна, необходимо задать им всем значения свойства Grouplndex, отличные от тех, которые имеют разделы меню дочерней формы. Если этого не сделать, то в процессе выполнения при открытии первого же дочернего окна меню Окно заменится на меню дочерней формы Файл. Следовате- льно, дальнейшее управление окнами будет потеряно. Это прои- зойдет потому, что по умолчанию значения свойства Grouplndex для всех разделов всех меню равно 0. Если мы хотим, чтобы в процессе выполнения приложения меню дочерней формы Файл встраивалось перед меню Окно, необ- ходимо всем разделам меню Окно присвоить значение Groupln- dex, большее, чем 0, соответствующий разделам дочернего меню. 13. Во всех введенных разделах меню Окно установите свойство Grouplndex равным 1. 14. Запишите обработчик события OnClick для раздела меню Но- вое (MNew). Этот обработчик может иметь вид, уже рассмот- ренный в предыдущем разделе: procedure TFMDI.MNewClick{Sender: TObject); var NewF : TFDoc; begin NewF := TFDoc.Create(Application); NewF.Caption := ’Окно ' + IntToStr(MDIChildCount); NewF.Show; end;
Формм 137 15. Внесите операторы в обработчики событий OnClick для оста- льных разделов меню. Для раздела Каскад: Cascade; Для раздела Упорядочить по горизонтали : TileMode := tbfiorizonta1; Tile; Для раздела Упорядочить по вертикали : TileMode := tbVertical; Tile; Для раздела Упорядочить значки: Arrangelcons; На этом проектирование многооконного редактора завершено. Можете сохранить проект и опробовать приложение в работе (см. рис. 5.3). Посмотрите, как ведет себя приложение при создании нового окна документа, если размеры родительского окна не до- статочно велики. Проверьте, что было бы, если бы вы не измени- ли свойство Grouplndex в меню родительского окна или если форма документов имела бы свойство FonnStyle равное fsNor- mal. Вообще поэкспериментируйте с этим приложением, чтобы уяснить все особенности приложений MDI. Обратите внимание на то, что ваше приложение автоматически удовлетворяет всем требованиям Windows К приложениям MDI. Наример, при раз- вертывании окна документа его заголовок автоматически пере- мещается в заголовок окна приложения, если для заголовков пе хватает места, они автоматически сворачиваются (см. рис. 5.3 б). 5.6 Объект Screen и приложения, работающие с нескольким мониторами В приложении Delphi автоматически создается глобальный объект Screen (экран) типа TScreen, свойства которого определя- ются из информации Windows о мониторе, на котором запускает- ся приложение- Вы можете в любом приложении использовать, например, такие параметры объекта Screen, как Height — высо- та экрана и Width — его ширина. Это, в частности, может потре- боваться, если вы задаете значение свойства своих форм Position таким, что размеры форм остаются постоянными. Так как вы ис- пользуете в процессе проектирования один тип монитора, а при- ложение в дальнейшем может работать на мониторе другого типа, то не исключено, например, что ваша форма не поместится на экране или наоборот — будет слишком маленького размера для данного монитора. Чтобы избежать этих неприятностей,
138 Глаио 5 можно автоматически месштабировать свою форму, вводя, на- пример, в обработчик ее события OnCreate код: Forml.Width := Screen.Width div 2; Forml-Height:= Screen.Height div 2; ' Этот код задает размеры формы равными половине соответст- вующих размеров экрана. Разрешающую способность экрзна можно определить, воспо- льзовавшись свойством PixelsPerlnch, указывающим количест- во пикселей экрана на дюйм в вертикальном направлении. Это справедливо именно для вертикального направления, поскольку во многих мониторах коэффициенты масштабирования по гори- зонтали и вертикали различаются. Screen имеет свойство Forms! I ], содержащее список форм, отображаемых в данный момент на экране, и свойство FormCo- unt, отражающее количество таких форм. Вы можете использо- вать это свойство, например, для того, чтобы гарантировать, что на данном тиле монитора раамеры ни одной формы не превысят размеров экрана. Соответствующий код может выглядеть так: with Screen do for I := 0 to FormCount-1 do begin if Forms[I].Height > Height then Forms[I]-Height := Height; if Forms[I].Width > Width then Forms [I] .Width :«= Width; end; Размеры форм, превышающие размер экрана, урезаются этим кодом. В приведенных примерах надо, конечно, предусмотреть, что- бы при изменении размеров формы адекватно изменялось и рас- положение компонентов на ее поверхности. Этот вопрос подробно рассмотрен в главе 2. Еще одно полезное свойство объекта Screen — Fonts (шриф- ты). Это свойство типа TStrings содержит список шрифтов, до- ступных на данном компьютере (свойство только для чтения). Его можно использовать в приложении, чтобы проверять, имеет- ся ли на компьютере тот или иной шрифт, используемый в при- ложении. Если нет — то можно или дать пользователю соответст- вующее предупреждение, или сменить шрифт в приложении на один из доступных, или дать пользователю возможность самому выбрать соответствующий шрифт. Например, вы можете помес- тить в вашем приложении компонент списка ТСошЪоВох и при событии формы OnCreate загрузить его доступными шрифтами с помощью операторов:
Формы 139 ComboBoxl.Items:=Screen.Fonts; ComboBoxl.ItemIndex:=O; Тогда в нужный момент пользователь может выбрать подхо- дящий шрифт из списка, а для того, чтобы этот шрифт использо- вался, например, для текста в компоненте RichEditl, в обработ- чик события OnClick или OnChange списка вставьте оператор; RxchEditl.Font.Name :•» ComboBoxl.Items 1 ComboBoxl.Itemindex]; Если котите использовать выбранный шрифт для всех компо- нентов формы, в которых свойство ParentFont установлено в true, то приведенный выше оператор должен иметь вид: Font.Name := ComboBoxl. Items (ComboBoxl. Itemindex J ,- Свойство Cursor объекта Screen определяет вид курсора. Если это свойство равно crDefault, то вид курсора при перемещении над компонентами определяется установленными в них свойст- вами Cursor. Но если свойство Cursor объекта Screen отлично от crDefault, то соответствующие свойства компонентов отменяют- ся и курсор имеет глобальный вид, заданный в Screen. Этим можно воспользоваться для такой частой задачи, как изменение курсора на форму «песочные часы» во время выполнения ка- ких-то длинных операций. Это можно сделать следующим обра- зом (о примененной в коде структуре try...finally см. в разделе вы узнаете в разделе 1.10.3): Screen.Cursor := crHovrglass; try «выполнение требуемых длинных операций» finally Screen.Cursor:=crDefault; end; При успешном или аварийном окончании длинных операций курсор в любом случае возвращается в значение по умолчанию. Если в приложении в какие-то отрезки времени используется отличный от crDefault глобальный вид курсора, то приведенный код можно изменить, чтобы по окончании длинных операций восстановить прежнее глобальное значение: var OldCursor:TCursor; begin OldCursor : = Screen.Cursor; try <выполнение требуемых долгих операций» finally Screen.Cursor:= OldCursor; end;
140 ГладаS Имеется также свойство Cursors! I ], которое представляет со- бой список доступных приложению курсоров. Вы можете создать и использовать также свой собственный курсор. Создается он и включается в ресурс приложения встроенным в Delphi Редакто- ром Изображений (Image Editor). Как работать с этим редакто- ром поясняется в разделе 6-1.2. А регистрируется созданный вами курсор с помощью функции LoadCursor. Сделать это можно следующим образом. Пусть, например, вы создали свой курсор и включили его в ре- сурс приложения под именем MyCursorl. Тогда в своем прило- жении вы можете ввести глобальную константу, обозначающую ваш курсор. Например: implementation const crMyCursor = 1; Значение этой константы может лежать в пределах от -32768 до 32767. Но важно, чтобы она не совпадала с предопределенны- ми значениями стандартных курсоров, лежащими в диапазоне от 0 до -21. В обработчике события OnCreate формы вы можете ввести оператор, регистрирующий ваш курсор в свойстве Cursors: Screen.Cursors[crMyCursor]:=LoadCursor(HInstance, MyCursorl'J; Тогда в нужный момент вы можете установить этот курсор в качестве глобального оператором Screen.Си rsor:=crMyCursor; а затем восстановить значение глобального курсора оператором Screen.Cursor:=crDefault; Вы можете использовать ваш зарегистрированный курсор и как локальный, например, для панели Panall оператором Panell.Cursor:=crMyCursor; С помощью Screen можно получить доступ к активной в теку- щий момент форме вашего приложения через свойство Active- Form. Если в данный момент пользователь переключился с ва- шего приложения на какое-то другое и, следовательно, ни одна форма вашего приложения не активна, то ActiveForm указывает на форму, которая станет активной, когда пользователь вернется к вашему приложению. В момент переключения фокуса с одной вашей формы на другую, генерируется событие OnActiveFormC- hange. Аналогично с помощью свойства ActiveControl можно полу- чить доступ к активному в данный момент оконному компоненту
Формы 141 на активной форме. При смене фокуса генерируется событие On Act i veControlChange. Начиная c Delphi 4 предусмотрена возможность разработки мультиэкранных приложений, работающих одновременно с мно- жеством мониторов. При этом приложение может решать, какие формы и диалоги надо отображать на том или ином мониторе. Свойства различных мониторов, используемых в таком приложе- нии, можно найти с помощью свойства Screen.Monitors[ I ], где I — индекс монитора. Индекс 0 относится к первичному монито- ру. Свойство Screen.Momtors[ I ] является списком объектов типа TMonitor, содержащих информацию о конкретных монито- рах. Среди свойств объектов типа TMonitor имеются Height — вы- сота и Width — ширина экрана монитора. Кроме того имеются свойства Left и Тор. Эти свойства означают следующее. Все до- ступное экранное пространство можно представить себе разби- тым на экраны отдельных мониторов, размещающихся слева на- право и сверху вниз. Соответственно свойства Left и Тор опреде- ляют координаты левого верхнего угла экрана монитора в этом логическом экранном пространстве. Объекты типа TMonitor име- ют также свойство MonitorNum — номер монитора, представля- ющий собой его индекс в свойстве Screen.Momtors[ I ]. Для управления тем, на каком мониторе должна появляться та или иная форма, служит свойство формы DefaultMonitor. Это свойство может принимать значения: dmDesktop dmPrimary dniMainForm dmAetiveForm не предпринимается попыток разместить форму на конкретном мониторе форма размещается на первом моет горе в списке Screen.Monitors форма появляется ня том мониторе, на котором размещена главная форма форма появляется на том мониторе, на котором размещена текущая активная форма

Глава 6 Графика и мультимедиа 6.1 Построение графических изображений 6.1.1 Использование готовых графических файлов 6.1.1.1.Компонент Image и некоторые его свойства Нередко возникает потребность украсить свое приложение ка- кими-то картинками. Это может быть графическая заставка, яв- ляющаяся логотипом вашего приложения. Или это могут быть фотографии при разработке приложения, работающего с базой данных сотрудников некоего учреждения. В первом случае вам потребуется компонент Image, расположенный на странице Addi- tional библиотеки компонентов, во втором — его аналог DBImage, связанный с данными и расположенный на странице Data Cont- rols. Начнем знакомство с этими компонентами. Откройте новое приложение и перенесите на форму компонент Image. Его свой- ство, которое может содержать изображение — Picture. Нажми- те на кнопку с многоточием около этого свойства или просто сде- лайте двойной щелчок на Image, и перед вами откроется окно Picture Editor (рис. 6.1), позволяющее загрузить в свойство Pic- ture какой-нибудь графический файл (кнопка Load), а также со- хранить открытый файл под новым именем или в новом катало- ге. Щелкните на Load, чтобы загрузить графический файл. Перед вами откроется окно Load Picture, представленное на рис. 6.2. По мере перемещения курсора в списке по графическим файлам в правом окне отображаются содержащиеся в них картинки, а над ними — цифры, характеризующие размер картинки. Вы можете найти графические файлы в каталоге Images. В Delphi 4 и 5 он обычно расположен в каталоге ...\progrom files\Common Files\Bor- land Shared. В Delphi 3 он расположен в каталоге ...\progrom fi- les\Borland\Delphi 3, а в Delphi 1 — в каталоге Delphi! 6. К сожале- нию, в Delphi 1 окно загрузки изображения значительно менее удобное и не позволят предварительно просматривать файлы до их загрузки. На рис. 6.1 и 6.2 изображена загрузка файла ...\lma- ges\Splash\16Color\ecrih.bmp. После загрузки файла щелкните на ОК и в вашем компоненте Image отобразится выбранное вамп картинка. Можете запустить ваше приложение и полюбоваться
Когда вы в процессе проектирования загрузили изображение из файла в компонент Image, он не просто отображает его, но и сохраняет в приложении. Это дает вам возможность поставлять ваше приложение без отдельного графического файла. Впрочем, как мы увидим позднее, в Image можно загружать и внашние графические файлы в процессе выполнения приложения. Вернемся к рассмотрению свойств компонента Image. Если установить свойство AutoSize в true, то размер компо- нента Image будет автоматически подгоняться под размер поме- щенной в него картинки. Если же свойство AutoSize установлено в false, то изображение может не поместиться в компонент или, наоборот, площадь компонента может оказаться много больше площади изображения.
Графике и мультимедиа 145 Другое свойство — Stretch позволяет подгонять ие компонент под размер рисунка, а рисунок под размер компонента. Устано- вите AutoSize в false, растяните или сожмите размер компонен- та Image и установите Stretch в true. Вы увидите, что рисунок займет всю площадь компонента, но, поскольку вряд ли реально установить размеры Image точно пропорциональными размеру рисунка, то изображение исказится. Устанавливать Stretch в true может иметь смысл только для каких-то узоров, но не для картинок. Свойство Stretch не действует на изображения пик- тограмм, которые ие могут изменять своих размеров (см. раз- дел 6.1.1.3). Свойство — Center, установленное в true, центрирует изобра- жение на площади Image, если размер компонента больше разме- ра рисунка. Рассмотрим еще одно свойство — Transparent (прозрачность). Если Transparent равно true, то изображение в Image становит- ся прозрачным. Это можно использовать для-наложения изобра- жений друг на друга. Поместите на форму второй компонент Image и загрузите в него другую картинку. Только постарайтесь взять какую-нибудь мало заполненную, контурную картинку. Можете, например, взять картинку из числа помещаемых обыч- но на кнопки, например, стрелку (файл . \progrom files\common fi- les\borlond shored\imoges\buttons\orrowlr.bmp). Передвиньте ваши компоненты Image так, чтобы они перекрывали друг друга, и в верхнем компоненте установите Transparent равным true. Вы увидите, что верхняя картинка перестала заслонять нижнюю. Одно из возможных применений этого свойства — наложение на картинку надписей, выполненных в виде битовой матрицы. Эти надписи можно сделать с помощью встроенной в Delphi програм- мы Image Editor, которая будет рассмотрена в разделе 6.1.2. Учтите, что свойство Transparent действует только на бито- вые матрицы (о типах графических файлов см. в разделе 6.1.1.3). 6.1.1.2 Простое приложение для просмотра графических файлов Вы создали приложение, в котором на форме отображается выбрениая вами в процессе проектирования картинка. Вы може- те легко превратить его в более интересное приложение, в кото- ром пользователь сможет просматривать и загружать любые гра- фические файлы. Для этого достаточно перенести на форму ком- понент OpcnPictureDialog, расположенный в библиотеке на странице Dialogs и вызывающий диалоговое окно открытия и предварительного просмотра изображения (рис. 5.2), а также кнопку, запускающую просмотр, или меню с единственным раз-
делом Файл. Если вы работаете с более младшей версией, чем Del- phi 4, то вместо компонента OpcnPictnrcDialog вам придется взять с той же страницы библиотеки обычный диалог открытия файла OpenDialog. Правда, при этом у пользователя не будет функции предварительного просмотра изображения. И взм при- дется проделать одну дополнительную операцию: установить в OpenDialog свойство Filter равным «графические файлы (*-jpg»*-ipeg;*-binp;*.ico;*-emf;*.wmf)!*.jpg;*.jpeg;*.bmp;*.ico;*.e А теперь вам осталось написать всего один оператор в обработ- чике щелчка на кнопке или на разделе меню: if OpenPictureDialogl.Execute then Imagel.Picture.LoadFromFile( OpenPictureDialogl.FileName); Этот оператор загружает в свойство Picture компонента Ima- gel файл, выбранный в диалоге пользователем. Выполните свое приложение и проверьте его в работе. Щелкая нн кнопке вы мо- жете выбрать любой графический файл и загрузить его в компо- нент Imagel. В таком приложении есть один недостаток — изображения могут быть разных размеров и их положение на форме или будет несимметричным, или они не будут помещаться в окне. Это лег- ко изменить, заставив форму автоматически настраиваться на размеры изображения. Для этого надо установить в компоненте Imagel свойство AutoSize равным true, а приведенный ранее оператор изменить следующим образом: if OpenPictureDialogl.Execute then begin Imagel-Picture-LoadFromFile(OpenPictureDialogl.File- Name) ; Forml.ClientHeight := Imagel.Height*10; Imagel. Top:= Forml.ClientRect.Top ♦(Forml.ClientHeight-Imagel-Height) div 2: Forml.ClientWidth := Imagel-Width+10; Imagel.Left:= Forml.ClientRect.Left + (Forml .ClientWidth-Imagel.Width) div 2; end; В этом коде размеры клиентской области формы устанавлива- ются несколько больше размеров компонента Imagel, которые в свою очередь адаптируются к размеру картинки благодаря свой- ству AutoSize. Запустите теперь ваше приложение, и вы увидите (рис. б.З), что при различных размерах изображения ваше приложение вы- глядит отлично.
Графика и мультимедиа Рис. 6.3 Адоптация формы к размерам изображения 6Л. 1.3 Форматы графических файлов Прежде, чем продвигаться дальше, поговорим немного о фор- матах графических файлов. Delphi поддерживает три типа фай- лов — битовые матрицы, пиктограммы и метафайлы. Все три типа файлов хранят изображения; различие заключается лишь в способе их хранения внутри файлов и в средствах доступа к ним. Битовая матрица (файл с расширением .bmp) отображает цвет каждого пикселя в изображении. При этом информация хранит- ся таким образом, что любой компьютер может отобразить кар- тинку с разрешающей способностью и количеством цветов, соот- ветствующими его конфигурации. Пиктограммы (файлы с расширением Лео) — это маленькие битовые матрицы. Они повсеместно используются для обозначе- ния значков приложений, в быстрых кнопках, в пунктах меню, в различных списках. Способ хранения изображений в пиктограм- мах схож с хранением информации в битовых матрицах, но име- ются и различия. В частиости, пиктограмму невозможно масш- табировать, она сохраняет тот размер, в котором была создана. Метафайлы (Metafiles) хранят не последовательность битов, из которых состоит изображение, а информацию о способе созда- ния картинки. Они хранят последовательности команд рисова- ния, которые и могут быть повторены при воссоздании изображе- ния. Это делает такие файлы, как правило, более компактными, чем битовые матрицы. Delphi может работать со следующими файлами: Тип файла Расширение JPEG Image File -jpg, -jpeg Битовые матрицы (Bitmaps) .bmp Пиктограммы .ico Enhanced Metafiles 1 .emf Metafiles .wmf
148 Глена 6 6.1.1.4 Классы для хранения графических объектов TPicture, TBitMap, Tlcon и TMetafile Выше были рассмотрены типы графических файлов. Для хра- нения графических объектов, содержащихся в битовых матри- цах, пиктограммах и метафайлах, в Delphi определены соответ- ствующие классы — TBitMap, Tlcon и TMetafile. Все они явля- ются производными от абстрактного базового класса графиче- ских объектов TGraphic. Кроме того определен класс, являю- щийся надстройкой над TBitMap, Tlcon и TMetafile и способный хранить любой из этих объектов. Это класс TPicture, с которым вы уже познакомились в Непале этой главы. Он имеет свойство Graphic, которое может содержать и битовые матрицы, и пиктог- раммы, и метафайлы. Более того, он может содержать и объекты определенных пользователем графических классов, производ- ных, от TGraphic. Для доступа к графическому объекту можно использовать свойство TPicture.Graphic, но если тип графиче- ского объекта известен, то можно непосредственно обращаться к свойствам TPicture.BitMap TPicture. Icon или TPicture.TMetafile. Если вы создали свой собственный класс графических объек- тов, то обращаться к его свойствам и методам можно следующим образом: (Gra£ic as МуClass).MyProperty где MyClass — имя введенного вами класса, a MyProperty — имя свойства. Для всех рассмотренных классов определены методы загрузки из файла и сохранения а файл: procedure LoadFromFile{const Filename: string)г procedure SaveToFile(const FileName: string); При этом для классов TBitMap, Tlcon и TMetafile формат файла должен соответствовать классу объекта. Объект класса TPicture может оперировать с любым форматом. Для всех рассмотренных классов определены методы присваи- вания значений объектов: procedure Assign(Source: TPerslatent); Однако, для классов TBitMap, Tlcon и TMetafile присваивать можно только значения однородных объектов: соответственно битовых матриц, пиктограмм, метафайлов. При попытке присво- ить значения разнородных объектов генерируется исключение EConvertError. Класс TPicture — универсальный, ему можно присваивать значения объектов любых из остальных трех клас- сов. А значение TPicture можно присваивать только тому объек- ту, тип которого совпадает с типом объектв, хранящегося в нем.
Гро фико и мультимедиа Приведем пример. Часто в приложениях создается объект типа TBitMap, назначение которого — запомнить содержимое графического изображения и затем восстанавливать его, если оно будет испорчено или изменено пользователем. Код, решающий эту задачу, может иметь вид; var BitMap:TBitMap; procedure TForml.FormCreate{Sender: TObject); begin BitMap:=TBitMap.Create; BitMap.LoadFrcmFile(’...’); Image1.Picture.Assign(BitMap); end; procedure TForml.FormDestroy(Sender; TObject); begin BitMap.Free; end; procedure SaveClick(Sender: TObject); begin BitMap.Assign(Imagel.Picture); procedure RestoreClick(Sender: TObject); begin Imagel.Picture.Assign(BitMap); end; В этом коде сначала объявляется переменная BitMap типа TBitMap. Затем в момент создания формы (при событии формы OnCreate) в процедуре TForml.FormCreate создается объект Bit- Map и в него методом LoadFromFile загружается изображение из указанного файла. Затем оператор Imagel.Picture.Assign(BitMap); присваивает значение графического объекта свойству Picture компонента Imagel. Изображение тем самым делается видимым пользователю. Этот оператор можно записать ниаче: Imagel.Picture.BitMap.Assign(BitMap); что даст тот же самый результат. Если вы создали объект BitMap, то надо не забыть его уничто- жить при окончании работы и освободить от него память Авто- матически это не делается. Поэтому надо освобождать память, например, в обработчике события формы OnDestroy (процедура FormDestroy) методом Free: BitMap.Free;
150 Глава 6 Если надо переписать в Bit Мар отредактированное пользова- телем в Imagel изображение, это можно сделать оператором (процедура SaveClick): Bitmap.Assign(Imagel.Picture); Если же надо восстановить в Imagel прежнее изображение, испорченное по кеким-то причинам, то это можно сделать опера- тором (процедура RestoreClick): Imagel-Picture.Assign(BitMap); Таким образом, мы видим, что методом Assign можно копиро- вать изображение из одного однотипного графического объекта в другой и обратно. Загружать изображения можно не только из файлов, но и из ресурсов приложения с помощью методов procedure LoadFromResourceNarne(Instance: THandle; const ResName: string); или procedure LoadFromResourcelD(Instance: THandle; ResID: Integer); где ResName — имя графического объекта в файле ресурса, a Re- sID — его идентификатор. Например, оператор BitMapl.LoadFromResourceNarne(HInstance,'MYBITMAP'); загружает в объект BitMap 1 из ресурса битовую матрицу с име- нем «MYBITMAP*. Как создавать в ресурсах битовые матрицы и другие графические объекты будет рассказано в разделе 6.1.2.4. Имеются еще методы загрузки и выгрузки графических объ- ектов в поток и в буфер обмена Clipboard, подобные методам ра - боты с файлами. Вы можете посмотреть их во встроенной справке Delphi. 6.1.2 Редактор Изображений Image Editor 6.1.2.1 Создание файла изображения В Delphi имеется встроенный Редактор Изображений — Image Editor, который вызывается командой Tools | Imoge Editor. Окно Редактора Изображений представлено на рис. 6.4 а. Это сравни- тельно простой редактор с не очень богатыми возможностями. Он позволяет создавать изображения в виде битовых матриц, пик- тограмм, изображений курсоров и не только сохранять создан- ные изображения в виде файлов, но и сразу включать их в файл ресурсов приложения. В этом в основном и заключается его отли- чие от других, более мощных графических редакторов. Работа начинается с раздела меню File, в котором вы можете выбрать раздел Open — открыть новый файл изображения или
151 ресурсов, или раздел New -— создать новый файл. Если вы выбра- ли New, то вам предлагается сделать дополнительный выбор, определяющий вид файла, который вы хотите создать: Resource File (.res) файл ресурсов Component Resource File (.dcr) файл ресурсов компонента Bitmap File (.bmp) файл битовой матрицы Icon File (.ico) файл пиктограммы Cursor File (.cur) файл изображения курсора Пусть, например, вы хотите создать свой рисунок для битовой матрицы. Тогда, выбрав раздел Bitmap File, вы попадаете в окно (рис. 6.4 б), в котором должны выбрать размер (Size) матрицы по горизонтали (Width) и вертикали (Height), а также выбрать набор цветов: 2, 16 или 256. Вероятно, для начала вам будет вполне до- статочно 16 цветов. После сделанного выбора вы увидите в окне Редактора Изоб- ражений границы вашего будущего рисунка, как это показано на
152 Глава 6 рис. 6.4 а. Вы можете начинать творить. Раздел меню View предо- ставляет вам возможность увеличить изображение в 2 раза (раз- дел Zoom In), уменьшить ранее увеличенное изображение (раз- дел Zoom Out) или посмотреть изображение в его реальном разме- ре (раздел Actuol Size). Расположенная слева инструментальная панель предоставля- ет вам следующий инструментарий, достаточно типичный для любого графического редактора: Выделение прямоугольной области рисунка, которую затем можно передвинуть мышкой, скопировать или вырезать в бу- фер обмена и т.п. Выделение произвольной области рисунка, которую затем можно передвинуть мышкой, скопировать или вырезать в бу- фер обмена и т.п. Просмотр отдельных пикселей — выделение прямоугольной области рисунка, которая затем увеличивается настолько, что можно работать с отдельными пикселями. & Ластив, перемещение которого стирает изображение, окраши- вая пиксели вспомогательным цветом, если нажата левая кнопка мыши, или основным цветом, если нажата правая кнопка мыши. । Карандаш, перемещение которого наносит линию основным цветом, если нажате левая кнопка мыши, или вспомогатель- ным цветом, если нажате правая кнопка мыши. Толщина ли- нии выбирается из набора, расположенного внизу инструмен- тальной панели. .S Кисть, перемещение которой окрашивает поверхность основ- ным цветом, если нажата левая кнопка мыши, или вспомога- тельным цветом, если нажата правая кнопка мыши. Форма кисти выбирается из набора, расположенного внизу инстру- ментальной панели. с Пульверизатор. Цвет зависит от нажатой кнопки мыши. Фор- ма шатен выбирается из набора, расположенного внизу инстру- ментальной панели. т Ввод текста. Перад началом ввода или сразу в момент оконча- ния можно пользуясь меню Text выбрать тип и размер шрифта. Заполнитель, заливающий выбранным цветом любой нарисо- ванный замкнутый контур иви всю поверхность изображения. Индикатор цвета, показывающий цвет пикселя, на который он указывает. Его надо подвести к пикселю, цвет которого вы хо- тите выбрать, и щелкнуть левой или правой кнопкой мыши, если хотите соответственно назначить этот цвет основным, или вспомогательным.
Графики и мультимедиа 153 Кроме перечисленных инструментов на инструментальной па- нели вы можете видеть кнопки, соответствующие рисованию прямых линий, дуг, незаполненных и заполненных прямоуголь- ников, прямоугольников со скругленными углами, эллипсов. При выборе таких инструментов, как карандаш, кисть, пуль- веризатор, кнопки рисования линий, дуг, незаполненных фигур внизу появляется палитра, позволяющая выбрать толщину ли- нии или форму кисти. В нижней части Редактора Изображений (рис. 6.4 а) располо- жена палитра цветов. В ее левой части имеются два квадрата. Цвет левого из них — назовем его основным, используется, если при рисовании вы нажимаете левую кнопку мыши; цвет право- го — вспомогательный, используется, если при рисовании вы на- жимаете правую кнопку мыши. Вот, собственно, и все премудрости. Если вы обладаете худо- жественными способностями (я, увы, ими не обладаю), то можете попробовать нарисовать что-нибудь стоящее. Если же нет. то мо- жете воспользоваться каким-нибудь готовым файлом -Ьгор (команда File (Open позволит вам его открыть) и что-то к нему до- бавить, например, текст. А еще проще — напишите просто текст и сохраните в виде файла .Ьгор. В дальнейшем вы можете нало- жить его в своем приложении из любой рисунок с помощью свой- ства Transparent компонента Image, как было рассказано в раз- деле 6.1.1.1. Файл пиктограммы создается аналогично. Вы можете создать, например, несложный файл пиктограммы и ватем использовать его как вашу фирменную пиктограмму во всех своих приложени- ях. 6.1.2.2 Создание пиктограммы для шаблона компонента в библиотеке Рассмотрим отдельно возможную процедуру создания собст- венных пиктограмм для библиотеки компонентов, если вы поме- щаете туда новый компонент ини шаблон (пример создания шаб- лона приведен в разделе 1.6.2). Эти пиктограммы должны иметь формат файла .bmp разме- ром 24 на 24. Вы можете, конечно, нарисовать сами нужную пик- тограмму, если сумеете. Но поскольку у меня, например, худо- жественные способности отсутствуют, то мне представляется бо- лее легким и более качественным следующий путь. Можно создать новое приложение Delphi, перенести на форму компонент, похожий на тот, для которого вы хотите создать пик- тограмму, и изменить в нем то, что надо. При этом надо умень-
154 Глава 6 шить его размеры настолько, чтобы он нормально помещался в размер 24 на 24. Если компонент содержит символы, то при необ- ходимости можно уменьшить соответственно размер шрифта (в этом вам часто может помочь шрифт Small Fonts). Затем вы запус- каете приложение на выполнение и нажимаете клавиши Alt-Prinl Screen. В результате изображение окна вашего приложе- ния будет записано в буфер обмена. Аналогично можно посту- пить, если похожую на требуемую вам пиктограмму вы увидите в каком-то из стандартных приложений Windows. Вы можете на- пустить это приложение и клавишами Alt-Print Screen занести его изображение в буфер обмена. После этого откройте Редактор Изображений (Fools | Image Edi- tor), выполните команду File | New | Bitmap File (-bmp) и в открыв- шемся окне (рис. 6.4 б) задайте размер 24 на 24. Перед вами от- кроется окно заготовки вашего рисунка. Выполните команду Edit | Paste, которая скопирует изображение из буфера обмена в вашу заготовку рисунка. Остается только мышью сдвинуть его так, чтобы изображение интересующего вас компонента попало в центр рамки рисунка. Затем можете внести в изображение нуж- ные коррективы и, выполнив команду File | Sove As, сохранить ри- сунок в файле. Далее можете использовать его как пиктограмму шаблона в библиотеке компонентов, выбрав его кнопкой Change в окне на рис. 1.12 (см. раздел 1.6.2). Мы рассмотрели создание пиктограммы для включаемого в библиотеку нового шаблона компонента. Пиктограммы новых компонентов делаются точно так же, но они выполняются не в виде отдельных файлов, а включаются в файлы ресурсов, о кото- рых пойдет речь ниже. 6.1.2.3 Создание пиктограммы для кнопки Теперь мы попробуем создать пиктограмму для кнопки. Такие кнопки, как SpeedButton и BitEtn. могут воспринимать пиктог- раммы, загружаемые в их свойство Gliph. Одно изображение пиктограммы имеет размер 16x16. Но в одном файле может со- держаться до четырех изображений такого размера. Самое левое соответствует отжатой кнопке. Второе слева соответствует недо- ступной кнопке, когда ее свойство Enabled равно false. Третье слева изображение используется при нажатии пользователя па кнопку при ее включении. Четвертое слева изображение исполь- зуется в кнопках с фиксацией (в SpeedButton) для изображения кнопки в нажатом состоянии. Давайте сденаем пиктограмму по полной программе — на 4 изображения. Создайте новый файл битовой матрицы
Графика и мультимедиа 155 (File | New | Bitmop File (.bmp)) и в открывшемся окне (рис. 6.4 б) за- даете размер 64x16. Разбейте изображение на 4 квадрата, разде- ленных белыми рамками в один пиксель (рис. 6.5 а), и закрасьте их в последовательности слева направо красным, серым, желтым и зеленым цветами. Рис. 6.5 Создание пиктограммы для и ее использование При создании файла пиктограмм для кнопок надо иметь в виду, что левый нижний пиксель задает цвет «прозрачности», т.е. цвет, который будет заменяться цветом поверхности кнопки. Поэтому, если вы, например, просто закрасите первый квадрат, не задав ему рамку, то он не будет виден на кнопке. Сохраните созданный файл и постройте простое приложение, чтобы посмотреть ваши пиктограммы в работе. Перенесите на форму (рис. 6-5 б) три кнопки: SpeedButton, BitBtn и Button. В свойство Gliph кнопок SpeedButton и BitBtn загрузите ваш файл пиктограмм. В свойстве Caption кнопки BitBtn задайте ка- кую-нибудь надпись, например «BitBtn». При этом вы сможете увидеть, что в свойствах кнопок NumGliphs установится равным 4. Поварьируйте свойствами Margin и Spacing кнопок, чтобы по- лучить симметричное размещение пиктограмм и надписей. Для кнопки SpeedButton установите свойств Grouplndex равным 1, а свойство AllowAllUp в true. В обработчик щелчка кнопки But- ton вставьте код: SpeedButtonl-Enabled:= not SpeedButton1.Enabled; BitBtn1.Enabled:= not BitBtnl.Enabled; который будет переключать свойство доступности кнопок с пик- тограммами. Теперь выполните приложение и посмотрите, как будут меняться цвета кнопок при различных манипуляциях с
156 Глово 6 ними. В отжатом состоянии пиктограммы будут красными. В мо- мент нажатия они окрашиваются в желтый цвет. У нажатой кнопки SpeedButton цвет пиктограммы будет зеленым. А в недо- ступных кнопках цвет пиктограмм будет серым. 6.1.2.4 Работа с файлами ресурсов В предыдущих разделах рассматривалась последовательность создания новых файлов картинок и пиктограмм. Теперь рассмот- рим случай, когда требуется включить картинку, пиктограмму или курсор в файл ресурсов какого-то проекта или компонента. Файлы ресурсов проектов имеют расширение .res и содержат би- товые матрицы (.bmp), пиктограммы (.ico), изображения курсо- ров (.сиг), используемые в проекте. Файлы ресурсов компонентов имеют расширение .dcr (dynamic component resource — динами- ческие ресурсы компонента) и могут включать такие же элемен- ты, как и файлы .res. В некоторых случаях включение изображений в файл ресур- са — единственная возможность решить ту или иную задачу. На- пример, если вы хотите ввести в своем приложении какой-то не- стандартный курсор, это можно сделать, зарегистрировав его с помощью функции LoadCursor в свойстве Cursors компонента Screen (необходимые для этого операции подробно обсуждаются в разделе 5.6). Однако, для использования своего курсора надо сначала создать его и включить в ресурс приложения с помощью Редактора Изображений. Другой пример — создание пиктограм- мы дня нового компонента (не шаблона), включаемого вами в библиотеку Delphi. Эта пиктограмма также должна быть создана не в виде отдельного файла, а как элемент ресурса компонента. Работа с файлом ресурса приложения в Редакторе Изображе- ний обычно начинается командой File | Open, открывающей файл ресурсов приложения .res, в котором вы хотите что-то изменить. Перед вами открывается окно, содержащее структуру файла в виде дерева (рис. 6.6). Сначала в нем может быть только один узел — Icon, содержащий вершину MAINICON, соответствующую стандартной пиктограмме приложения. Добавить новые узла вы можете с помощью команды Resource | New (ее вы можете найти и во всплывающем меню Редактора Изображений), выполняя ко- торую вам предоставляется возможность выбрать один из типов элементов: Bitmap, Cursor или Icon. Пусть, например, вы хотите использовать в своем приложении нестандартный курсор в виде человечка с укаакой. Вы выполняете Resource ] New | Cursor и в де- реве структуры файла ресурсов приложения появляется новая вершина. Вы должны задать ей то имя, которое в дальнейшем бу-
Графика и мультимед ио дете использовать в приложения при регистрации курсора функ- цией LoadCursor. Затем вы щелкаете на созданной вершине и вам открывается заготовка изображения курсора, в которой вы рисуете нужную картинку. После этого надо вернуться в окно структуры файла ресурса и выполнить команду File | Sove- Рис- 6.6 Структуре файле ресурсов приложения 6.1.3 Канва — холст для рисования 6.1.3.1 Канва и пиксели Многие компоненты в Delphi имеют свойство Canvas (канва, холст), представляющее собой область компонента, на которой можно рисовать или отображать готовые изображения. Это свой- ство имеют формы, графические компоненты Image, PaintBox, BitMap и многие другие. Канва содержит свойства и методы, су- щественно упрощающие графику Delphi. Все сложные взаимо- действия с системой спрятаны для пользователя, так что рисо- вать в Delphi может человек, совершенно не искушенный в ма- шинной графике. Каждая точка канвы имеет координаты X и Y. Система коор- динат канвы, как и везде в Delphi, имеет началом левый верхний угол канвы. Координата X возрастает при перемещении слева на- право, а координата Y — при перемещении сверху вниз. С координатами вы уже имели дело многократно, но пока вас не очень интересовало, что стоит за ними, в каких единицах они измеряются. Координаты измеряются в пикселях. Пиксель — это наименьший элемент поверхности рисунка, с которым можно
158 Глава 6 манипулировать. Важнейшее свойство пикселя — его цвет. Для описания цвета используется тип TColor (см. подробнее в разде- ле 1.4). 6.1.3.2 Рисование по пикселям Рисовать на канве можно разными способами. Первый вари- ант — рисование по пикселям. Для этого используется свойство канвы Pixels. Это свойство представляет собой двумерный мас- сив, который отвечает ва цвета канвы. Например, Canvas.Pi- xels[10,20] соответствует цвету пикселя, 10-го слева и 20-го сверху. С массивом пикселей можно обращаться как с любым свойством: изменять цвет, задавая пикселю новое значение, или определять его цвет по хранящемуся в нем значению. Например, Canvas.Pixels] 10,20] := 0 или Canvas.PixeIs{10,20] clBlack — это задание пикселю черного цвета. Давайте попробуем нарисовать график некоторой функции F(X) на канве компонента Imagel, если известен диапазон ее из- менения Ymax и Ymin и диапазон изменения аргумента Xmin и Хшах. Это можно сделать такой процедурой: var X,Y:real; // координаты функции FX,PYzlongint; // координаты пикселей begin for РХ:=0 to Imagel-Width do begin (X — координата, соответствующая пикселю с координатой РХ) X:=Xmin+PX*(Xmax-Xmin)/Imagel.Width; Y:=F(X); (PY — координата пикселя, соответствующая координате ¥} PY:=trunc(Imagel.Height-(Y-Ymin)*Imagel-Height/( Ymax-Ymin)} ; (Устанавливается черный цвет выОранного пикселя (О яркости)} Imagel.Canvas.Pixels[РХ,PY] := 0; end; end; В этом коде вводятся переменные X и Y, являющиеся значе- ниями аргумента и функции, а также переменные РХ и PY, яв- ляющиеся координатами пикселей, соответствующими X и Y. Сама процедура состоит из цикла по всем значениям горизонта- льной координаты пикселей РХ компонента Imagel. Сначала выбранное значение РХ пересчитывается в соответствующее зна- чение X. Затем производится вызов функции F(X) и определяет-
Гроф*шо и мультамедмс 159 ся ее значение Y. Это значение пересчитывается в вертикальную координату пикселя PY. И в заключение цвет пикселя с коорди- натами (РХ, PY) устанавливается черным. Попробуйте создать соответствующее приложение и посмот- реть, как оно работает. Пусть для простоты мы будем ориентиро- ваться на функцию sin(X), для которой Хпшз—О, Хшах=4я, Ymin—-1, Ymax=l. Начните новый проект, поместите на него компонент Image и кнопку с надписью Норисоеоть, в обработчик события OnClick ко- торой запишите код, аналогичный приведенному выше, но конк- ретизирующий функцию: X,Y:real; // координаты функции PX,PY:longint; // координаты пикселей begin for РХ:=О to Itnagel .Width do begin (X — координата, соответствующая пикселю с координатой РХ] X:- PX*4*Pi/Imagel.Width; Y:—Sin(X); {PY - координата пикселя, соответствующая координате Y] PY:-=trunc{Imagel .Height- (Y+l) *Imagel .Height/2) ; {Устанавливается черный цвет выбранного пикселя (О яркости)} Imagel.Canvas.Pixels[РХ,PY] := О; Откомпилируйте ваш проект, сохраните его (мы еще к нему вер- немся) и выполните. Результат представлен на рис. 6.7 в его ле- вой части. Правая часть будет рассмотрена в следующем разделе. Рис. 6.7 Рисовонив по пикселям и функцией LineTo
160 Глава 6 6.1.3.3 Рисование с помощью пера Реп У канвы имеется свойство Реп — перо- Это объект, в свою оче- редь имеющий ряд свойств. Одно из них уже известное вам свой- ство Color — цвет, которым наносится рисунок. Второе свойст- во — Width (ширина линии). Ширина задается в пикселях. По умолчанию ширина равна 1. Свойство Style определяет вид линии. Это свойство может принимать следующие значения: psSolid Сплошная линия psDash Штриховая линия psDol Пунктирная линия psDashDot Штрих-пунктирная линия psDashDotDot Линия, чередующая штрих и два пунктира psClear Отсутствие линии psInsideFratne Сплошная линия, но при Width > 1 допускающая цвета, отличные от палитры Windows Примеры линий всех стилей приведены на рис- 6.В. Линии различных стилей Все стили со штрихами и пунктирами доступны только при Width = 1. В противном случае линии этих стилей рисуются как сплошные. Стиль psInsideFrame — единственный, который допускает произвольные цвета. Цвет линии при остальных стилях округля- ется до ближайшего из палитры Windows. У канвы имеется свойство PenPos типа TPoint. Это свойство определяет в координатах канвы текущую позицию пера. Пере- мещение пера без прорисовки линии, т.е. изменение PenPos производится методом канвы MoveTo(X,Y). Здесь X и Y — коор- динаты точки, в которую перемещается перо. Эта текущая точка становится исходной, от которой методом LineTo(X.Y) можно провести линию в точку с координатами (X,Y). При этом теку-
Графика и мультимедиа 161 щая точка перемещается в конечную точку линии и новый вызов LineTo будет проводить точку из этой новой текущей точки. Давайте попробуем нарисовать пером график синуса из пре- дыдущего примера. Откройте прежний проект, добавьте па него еще один компонент Image и разместите компоненты так, как показано выше на рис. 6.7. Размеры обоих компонентов Image должны быть абсолютно одинаковы, так как на этом для эконо- мии размера кода и вашего труда основана программа, которую мы напишем. Сделать размеры компонентов абсолютно одинако- выми легко, выделив их оба и воспользовавшись командой всплывающего меню Size. Затем в уже написанном вами обработчике щелчка на кнопке добавьте перед началом цикла оператор Itnage2 -Canvas. MoveTo (0 , Image?. Height div 2) ; который переводит перо в начало координат второго графика — на левый край канвы в середину ее высоты. А перед заключите- льным end цикла добавьте оператор Itnage2. Canvas. LineTo (РХ, PY) ; который рисует на втором графике линию, соединяющую сосед- ние точки. Иначе говоря, теперь ваш код должен иметь вид: var X,У:real; // координаты функции РХ,PY:longint; // координаты пикселей begin Image?.Canvas.MoveTo(О,Image2.Heigh t div 2); for PX:=O to Imagel.Width do begin {X — координата, соответствующая пикселю с координатой РХ} Х:= PX*4*Pi/Imagel.Width: Y:=Sin(X); {PY — координата пикселя, соответствующая координате Y) PY:=trunc(Imagel.Height-(Y+l)*Imagel.Height/2); (Устанавливается черный цвет выбранного пикселя (О яркости}} Imagel.Canvas.Pixels[РХ,PY] := О; (Проводится линия на втором графике) Image2.Canvas.LineTo(РХ,PY>; end; end; Для экономии кода мы воспользовались тем, что оба графика у нас абсолютно одинакового размера и, следовательно, пересчет координат достаточно провести для одного из них, а потом воспо- льзоваться этими координатами для рисовании обоих графиков.
Откомпилируйте приложение и выполните его. Вы получите результат, представленный на рис. 6.7. Легко видеть, что качест- во двух одинаковых графиков сильно различается. В левом на крутых участках сплошной линии нет — она распадается па от- дельные точки — пиксели. А правый график весь сплошной. Это показывает, что при прочих равных условиях рисовать лучше не по пикселям, о пером. Отметим еще одно ценное свойство компонента Image и его канвы. Вы можете задавать координаты пикселей, выходящие за пределы размеров канвы, и ничего страшного при этом не слу- чится. Эго позволяет не заботиться о том, какая часть рисунка попадает в рамку Image, а какая нет. Вы можете легко проверить это, увеличив, например, вдвое размах вашей синусоиды. Для этого достаточно изменить оператор, задающий значение Y, на следующий: Y:=2*Sin(X); Вы получите результат, показанный па рис. 6.9. Изобразилась только та часть рисунка, которая помещается в рамку канвы. Эго позволяет легко осуществлять приложения, в которых пользова- телю предоставляется возможность увеличивать и просматри- вать в деталях какие-то фрагменты графиков. Рис. 6.9 Изображение при размерах изображения, превышающих размер канвы Перо может рисовать не только прямые линии, но и фигуры. Ниже перечислены некоторые из методов канвы, использующие перо для рисования фигур:
Графика и мультимедио 163 Arc Рисует дугу окружности или эллипса Chord Рисует замкнутую фигуру, ограниченную дутой окружности или эллипса и хордой Ellipse Pie Рисует окружность или эллипс Рисует сектор окружности или эллипса PolyBezier Рисует кусочную кривую третьего порядка с точным отображением первой и последней точки; число точек должно быть кратко 3 PolyBezierTo Рисует кусочную кривую третьего порядка с точным отображением последней точки; число точек должно быть на 1 меньше числа, кратного 3 Polygon Polyline Rectangle RoundRect Рисует замкнутую фигуру с кусочно-линейной границей Рисует кусочио-лииейную кривую Рисует прямоугольник Рисует прямоугольник со скругленными углами Примеры большинства перечисленных фигур приведены на рис. 6.10. Рис. 6.10 Примеры фигур, нарисованных пером Ниже приведен текст процедуры, которая рисовала фигуры, показанные на рис. 6.10. Этот текст поможет вам понять методы, осуществляющие рисование фигур. with Imagel.Canvas do begin Font.Style:=[fsBold]; Arc(10,10, 90, 90, 90, 50,10, 50) ; TextOut(40,60,’Arc’}; Chord(110,10,190,90.190,50,110,50); TextOut(135,60,’Chord');
Ellipse(210, 10,290, 50} ; TextOut(230,60, ’Ellipse’); Pie(310,10,390,90,390,30,310,30); TextOut(340,60,'Pie'); PolyGon([Point(30,150),Point(40,130),Point(50,140), Point(60,130),Point(10,150)]); TextOut(30,170,'PolyGon'); PolyLine([Point(130,150) .Point(140,130), Point(150,140), Point(160,130).Point(170,150)]): TextOut(130,170,'PolyLine'); Rectangle (230, 120,280, 160) ,- TextOut(230,170,'Rectangle'); RoundRect(330,120,380,160,20,20); TextOut (325,170, ' RoundRect *) ; end; Для вывода текста на канву в приведенном примере использо- ван метод TextOut, синтаксис которого: TextOut(X,У,<текст>}; Имеется еще несколько методов вывода текста, которые при- меняются реже. Все методы вывода текста используют свойство канвы Font — шрифт, с которым вы уже знакомы. В частности, в приведенном примере с помощью этого свойства установлен жир- ный шрифт надписей. 6.1.3.4 Brush — кисть У канвы имеется свойство Brnsh — кисть. Это свойство опре- деляет фон и заполнение замкнутых фигур на канве. Brush — это объект, имеющий в свою очередь ряд свойств. Свойство Color определяет цвет заполнения. Свойство Style определяет шаблон заполнения (штриховку) и может принимать значения: Значение Шаблон Значение Шаблон bsSolid bsCross уд bsClear bsDiagCrona м bsBDiagona) я bsUorizontal - -~= bsFDiagonal bs Vertical IIIIIIIIIIII Имеется еще одно свойство кисти — BitMap, определяющее нестандартное заполнение заданным шаблоном. Шаблон задает-
Графика и мультимедиа 165 ся битовой матрицей размером 8 на 8. Если для кисти задан шаб- лон BitMap, то заполнение производится именно этим шаблоном, независимо от значения свойства Style. Шаблон BitMap может создаваться в процессе выполнения приложения или, например, загружаться из файла, как в приведенном ниже примере: var Bitmap: TBitmap; begin Bitmap := TBitmap.Create; try Bitmap.LoadFromFile('MyBitmap.bmp'); Forml.Canvas.Brush.Bitmap := Bitmap; finally Imagel.Canvas.Brush_Bitmap := nil; Bitmap.Free; end; end; В этом примере создается объект Bitmap типа TBitmap и в него загружается битовая матрица из фала с именем MyBit- map.bmp. Затем свойству Imagel.Canvas.Bmeh.Bitmap присваи- вается указатель на этот объект. После этого загруженный шаб- лон можно использовать для заполнения фигур на канве Imagel. В конце кода свойству BitMap присваивается значение nil, после чего заполнение опять начинает определяться свойством Style. Затем объект Bitmap уничтожается, чтобы освободить занимае- мую им память. Имеются функции канвы, рисующие заполненные фигуры. Это, например, метод FillReet, объявленный как procedure FillReet(const Rect: TRect); Он заполняет заданным стилем или шаблоном прямоугольную область, заданную параметром Rect. Этот параметр имеет тип TRect. Для его задания проще всего использовать функцию Rect(Xl,Yl,X2,Y2), возвращающую структуру Rect с координа- тами углов, заданных параметрами (XI, Y1) и (Х2, Y2). Функцию FillReet удобно, в частности, использовать для сти- рания изображения. Например, оператор with Imagel do Canvas.FillReet(Rect(0,0,Width,Eeight)); очищает вею площадь канвы компонента Imagel. Кисть участвует в заполнении фигур ие только с помощью этой функции. Все перечисленные ранее методы рисования зам- кнутых фигур тоже заполняют их с помощью кисти. Это относит- ся к методам Chord, Ellipse, Pie, Polygon и др.
166 Гло«оЬ Имеется еще один интересный метод, работающий с кистью. Это метод FIoodFill, который заполняет замкнутую область на канве. Этот метод определен следующим образом: type TFillStyle = (fsSurface, fsBorder); procedure FIoodFill(X, Y: Integer; Color: TCol-or; FillStyle'.- TFillStyle) ; Точка с координатами X и Y является произвольной внутрен- ней точкой заполняемой области, которая может иметь произво- льную форму. Граница этой области определяется сочетанием па- раметров Color и FillStyle. Параметр Color указывает цвет, кото- рый используется при определении границы заполняемой облас- ти, а параметр FillStyle определяет, как именно по этому цвету определяется граница. Если FillStyle — fsSurface, то заполняет- ся область, окрашенная цветом Color, а на других цветах метод останавливается. Если FillStyle — fsBorder, то наоборот, запол- няется область окрашенная любыми цветами, не равными Color, а на цвете Color метод останавливается. Для определения области закрашивания можно использовать координаты и цвет одного из пикселей, расположенных внутри области (если FillStyle = fsSurface) или снаружи ее (если FillS- tyle — fsBorder). В следующем разделе 6.1.4 будет приведен при- мер использования метода FIoodFill. Имеется еще один метод канвы, связанный с кистью. Это ме- тод FrameRect. Он рисует на канве текущей кистью прямоуголь- ную рамку, не закрашивая ее. Синтаксис метода FrameRect: procedure FrameRect(const Rect: TRect); Параметр Rect определяет позицию и размеры прямоугольной рамки. Толщина рамки — 1 пиксель. Область внутри рамки ки- стью ие заполняется. Метод FrameRect отличается от рассмот- ренного ранее метода Rectangle тем, что рамка рисуется цветом кисти (в методе Rectangle — цветом пера) и область ие закраши- вается (в методе Rectangle закрашивается). 6.1.4 Пример построения собственного простого графического редактора Давайте попробуем использовать полученные нами сведения для построения собственного простенького графического редак- тора, воспроизводящего некоторые функции настоящих редакто- ров. 1. Начните новое приложение. 2. Перенесите на форму два компонента типа TIraage и располо- жите их в нижней левой части формы (рис. 6.11), придав квадратную форму, например, размером 20 на 20. Это будут
Графики и мультимедиа 167 окна основного и вспомогательного цветов. Имена этих компо- нентов будут Imagel и Image2. Рис. 6.11 Редактор изображений в роботе: тестовая картинка (о) и работа с изображением, загруженным из файла (б) 3. Перенесите на форму еще один компонент типа TImage и рас- положите его в верхней части формы, несколько отступив от левого края, где у нас будет инструментальная панель, и рас- тянув так, чтобы он занимал основную часть формы. Это будет холст для картинок. Имя этого компонента будет Image3. 4. Перенесите на форму еще один компонент типа TImage и рас- положите его внизу правее первых двух ва одном с ними уров- не. Это будет палитра цветов. Ее высоту задайте той же, что у первых двух компонентов, а длину — в 10 раз большую. Имя этого компонента будет Imaged. 5. Перенесите на форму кнопку типа TSpeedButton и располо- жите ее в верхнем левом углу формы. Эта кнопка будет соот- ветствовать кисти — типичному инструменту графических ре- дакторов. Назовите ее SBBrush. Установите у кнопки свойст- во Groupindex равным 1 и свойство AllowAllUp в true. Эти
[168 Глсвоб свойства обеспечат кнопке возможность фиксироваться в на- I жатом и ие нажатом состоянии. Желательно загрузить в свой- ство Glyph пиктограмму кисти (файл ...\lmages\Bub tons\brush.bmp). 6. Перенесите на форму еще одну кнопку типа TSpeedButton- и расположите ее ниже SIJBrush. Эта кнопка будет соответство- вать указателю цвета пиксели рисунка. Назовите ее SBColor. Установите у этой кнопки, как и у предыдущей, свойство Gro- uplndex равным 1 (это обеспечит, что только одна из кнопок SBBrush или SBColor может быть нажата) и свойство АПо- wAUUp в true. Желательно зегрузить в свойство Glyph пик- тограмму (например, файл ,.\lmages\ Buttons\one2one.bmp). 7. Перенесите на форму диалог OpenPictureDialog, если вы рабо- таете с Delphi 4, или OpenDialog (в этом случае задайте его свойство Filter равным *.bmp|*.bmp[ace файлы]*.*). 8. Перенесите на форму главное меню МашМепп. В меню задай- те раздел Файл с подразделом Открыть. Назовите этот подраз- дел МОреп. Задайте еще один раздел — Провка с подразделом Отменить. Назовите этот подраздел Undo. Теперь размещение компонентов закончено и можно писать обработчики событий. Начнем со второстепенных обработчиков. 9. В раздел implementation включите объявление переменной var BitMap:TBitMap; Эта переменная будет тем объектом, в котором будет сохраня- ться изображение, чтобы его можно было восстановить командой Отменить. ТО.Для события OnCreate формы напишите обработчик вида: procedure TForml.FormCreate(Sender: TObject); var HW,i:integer; begxn BitMap:=TBitMap-Create; {задание свойств кисти основного и вспомогательного цветов} Imagel-Canvas.Brush.Color:=clBla ck; ImageZ.Canvas.Brush.Color:=clWhite; (заполнение окон основного и вспомогательного цветов) with Imagel.Canvas do FillRect(Rect(O,O,Width,Height)); with ImageZ.Canvas do FilJLRect (Rect (0,0, Width, Height)) ; (задание ширины элемента палитры цветов) HW:=Image4.Width div 10; {закраска элементов палитры цветов) with Image4.Canvas do for i:=l to 10 do begin
case i of 1:Brush.Color:=clBlack ; 2: Brush. Color: =clAgua; 3:Brush.Color:=clBlue; 4 :Brush.Color:=clFuchsia; 5:Brush.Color:=clGreen; 6:Brush.Color:=clLime; 7:Brush.Color:=clMaroon; 8:Brush.Color:=clRed; 9:Brush.Color:=clYellow; 10:Brush.Color:“ClWhite; end; Rectangle((i-1)*HW,0,i*HW,Height); {рисование креста на холсте — только иля тестирования) with Image3 do begin Canvas.MoveTo(0,0); Canvas.LineTo(Width,Height); Canvas.MoveTo(0,Height); Canvas.LineTo(Width,C); end; BitMap.Assign(Image3.Picture); end; Обработчик длинный, ио смысл его достаточно прост. Сначала создается объект BitMap, в котором будет храниться первоначальное изображение. Затем задаются свойства кисти окон основного (Imagel) и вспомогательного (Image2) цветов: черный и белый. Окна заливаются соответствующим цветом с по- мощью функции FillReet. После этого формируется палитра цве- тов: для каждого элемента палитры задается свой цвет и элемент заполняется этим цветом с помощью функции Rectangle. Затем на холсте Image3 рисуется крест (рис. 6.11 а). Он имеет чисто де- монстрационный характер, чтобы можно было протестировать программу на простом изображении. В реальной программе этот рисунок не нужен и, если хотите, можно эти операторы не пи- сать. В конце нарисованное на канве сохраняется в объекте Bit- Map методом Assign. 11. В обработчик события формы OnDestroy запишите оператор BitMap.Free; который освобождает память при закрытии приложения. 12. Для подраздела меню Открыть в обработчик включите операто- ры: if OpenPictureDialogl.Execute then begin Image3.Picture.LoadFromFile( OpenPictureDialogl.FileName);
170 BitMap.Assign(Image3-Picture); end; Эти операторы загружают в компонент Image3 файл изобра- жения, который пользователь выбирает в диалоге, и запоминают изображение в BitMap. 13. Для подраздела меню Отменить в обработчик включите опера- тор: Image3.Picture.Assign(BitMap); Этот оператор восстанавливает на холсте изображение, сохра- ненное в BitMap. 14 В обработчик события OnClick кнопок SBBrush и SBColor ва- пишите оператор if (Sender as TSpeedButton).Down then BitMap.Ass ign(Image 3.Picture); Этот оператор запоминает текущий вид изображения перед началом работы с очередным инструментом. 15. В обработчик события OnMouseDown компонентов Image3 и Imaged вставьте код: if((Sender as TImage).Name=‘Imaged’) or SBColor.Down {режим установки основного и вспомогательного цветов) begin if(Button=mbLeft) then with Imagel.Canvas do begin (установка основного цвета) Brush.Color:=(Sender as TImage).Canvas.Pixels[X,Y]; FillRect(Peet(0,0,Width,Height)); else with Image2.Canvas do begin (установка вспомогательного цвета} Brush.Color:=(Sender as TImage).Canvas.Pixels[X,Y]; FillRect(Rect(0,0,Width,Height)); end; end else if SBBrush.Down then with Image3.Canvas do begin (режим закраски указанной области холста) if Button=mbLeft then Brush.Color:=Imagel.Canvas.Brush.Color else Brush.Color:=Image2.Canvas.Brush-Color; FloodFill(X,Y,Pixels[X,Y),fsSurface); end; Это основной код, осуществляющий как установку основного и вспомогательного цветов, так и функцию инструмента графи- ческого редактора — кисти. Если кнопка мыши нажата на па-
Графика и мульти мирна 171 литре цветов Imaged или если кнопка SBColor — кнопка кисти утоплена, то приложение находится в режиме установки цветов. При нажатой левой кнопке мыши цвет пикселя под курсором мыши передается в окно основного цвета, а при нажатии правой кнопки — в окно вспомогательного цвета. Если кнопка мыши нажата на холсте Image3 и при этом кноп ка SBBrush утоплена, то приложение находится в режиме закр ски указанной области рисунка. В этом случае в зависимости см нажатой кнопки мыши выбирается основной или вспомогатель ный цвет и функцией FloodFill производится закраска области, координаты внутренней точки которой указаны курсором мыши, а цвет — цветом пикселя, на который указывает мышь. Ваше приложение готово. В дальнейшем, после получения не- которых дополнительных сведений о графике, вы, если захотите, сможете вернуться к нему и пополнить новыми инструментами. Откомпилируйте приложение, сохраните его и выполните. Проверьте установку цветов. При щелчке левой или правой кнопкой мыши на палитре цветов у вас будет меняться соответст- венно основной или вспомогательный цвет. Если вы нажмете кнопку кисти в окне приложения, то при щелчке на холсте у вас должна закрашиваться указанная курсором область в основной или вспомогательный цвет (рис. 6.11 а). Если вы отпустите кноп- ку кисти в окне приложения и нажмете кнопку определения цве- тов, то при щелчке на холсте у вас основной или вспомогатель- ный цвет будет устанавливаться равным цвету холста под курсо- ром мыши. Если вы выполните команду Правка | Отменить, то изображе- ние вернется к тому виду, какой был при начале работы с послед- ним использованным инструментом. Теперь вы можете загрузить командой Файл | Открыть вашего приложения какое-то изображение (на рис. 6.116 загружен файл ...\lmages\Splo5h\16Color\ecrth Ьглр). Вы можете попробовать пере- крашивать те или иные области изображения. Например, можете изменить цвет фона, выделить какую-то сплошную область ри- сунка одного цвета (на рис. 6.11 б выделена одна из областей об- лачного покрова) и т.п. Итак, вы реализовали два из основных инструментов любого графического редактора рисунков: кисть и индикатор цвета. Реа- лизация остальных типичных инструментов требует предварите- льного ознакомления с режимами рисования, что будет сделано в следующем разделе 6.1.6. После этого вы сможете при желании вернуться к вашему графическому редактору и попробовать реа-
172 Глава 6 лизовать, например, инструменты, рисующие различные фигуры и перемещающие фрагменты изображения. 6.1.5 Режимы рисования 6.1.5.1 Режимы пера У пера Реп имеется еще одно свойство, которое мы пока не рассматривали. Это свойство — Mode (режим). По умолчанию значение Mode = pmCopy. Это означает, что линии проводятся цветом, заданным в свойстве Color. Но возможны и другие режи- мы, в которых учитывается не только цвет Color, но и цвет соот- ветствующих пикселей фона. Наиболее интересным из этих ре- жимов является режим pmNotXor — сложение с фоном по ин- версному исключающему ИЛИ. Операция инверсного исключаю- щего ИЛИ аналианрует по битам два своих операнда. Результи- рующий бит равен «О», если соответствующие биты двух операн- дов не равны друг другу, а при равенстве битов операндов резуль- тирующий бит равен «1». Вспомните, что каждый пиксель хранит цвет как набор битов. Пусть, например, фоновый пиксель имеет значение 0110011, а цвет пера установлен в 1111О00. Применение операции pmNot- Xor к этим двум числам даст цвет со значением 0110100. Этот цвет перо задаст данному пикселю. А теперь посмотрим, что по- лучится, если перо повторно пройдет по тому же пикселю. В этом случае опять будет выполнена операция pmNotXor по отноше- нию к цвету пера 1111000 и текущему цвету пикселя, который стал равен 0110100. Применение pmNotXor к этим числам даст в результате 0110011, т.е. первоначальный цвет пикселя. Это еначит, что если нарисовать на фоне какую-то фигуру один раз, а затем нарисовать ту же фигуру повторно, то нарисо- ванная фигура исчезнет и каждый пиксель вернется к своему первоначальному цвету. Эту особенность режима pmNotXor, свойственную также режиму ршХог — сложение с фоном по иск- лючающему ИЛИ, можно использовать для создания простень- кой анимации. Достаточно нарисовать нечто, затем стереть нари- сованное, перерисовать немного измененным — и рисунок будет представляться ожившим. В последующих разделах мы часто бу- дем использовать такой режим рисования. 6.1.5.2 Режимы копирования и рисования канвы Ранее мы рассматривали и использовали копирование одного графического объекта в другой методом Assign. Однако, у канвы имеются и другие методы копирования. Это прежде всего метод CopyRect, позволяющий копировать прямоугольную область ис-
Графике и мультимедиа 173 точника изображения в прямоугольную область данной канвы. Метод определен следующим образом: procedure CopyRect(Dest: TRect; Canvas: TCanvas; Source: TRect}; Параметр Dest определяет прямоугольную область канвы, i. которую производится копирование. Параметр Canvas указывае । источник, из которого копируется изображение. Это может быт i канва любого компонента: типа Tlmage, типа TBitMap и др. )' частном случве источником может быть и канва того же компа пента, в который производится копирование. Параметр Source определяет прямоугольную область в источнике изображения, которая копируется в область Dest. Копирование — это не просто перенос изображения. В общем случае копирование означает сложное взаимодействие копируе- мого изображения и того, которое было до этого в области, куда производится копирование. Характер этого взаимодействия определяется параметром CopyMode (режим копирования) той канвы, в которую производится копирование. По умолчанию значение CopyMode равно cmSrcCopy. Это единственный режим, при котором производится действительное копирование: изобра- жение в Dest стирается и заменяется скопированным. Есть два значения — cm Whiteness и cmBIackness, при которых собствен- но никакое копирование не производится: просто область закра- шивается соответственно белым или черным цветом. А все оста- льные режимы определяют сложное взаимодействие копируемо- го изображения с тем, которое было в Dest. Особый интерес пред- ставляет режим cmSrcInvert, при котором изображения канвы и источника комбинируются, используя булеву операцию XOR. Так же, как мы видели это для пера, повторное копирование в подобном режиме восстанавливает прежнее изображение на кан- ве. Приведем примеры копирования в различных режимах. Опе- раторы Imagel.Canvas.CopyMode: = cmSrcCopy; Imagel.Canvas.CopyRect(Rect(0,0,200,200),Image2.Canvas, Rect (0,0,200,200)) ; обеспечивают копирование изображения фрагмента канвы ком- понента Image2 в указанную область канвы компонента Imagel. Изображение, которое ранее было на канве компонента Imagel в пределах области с координатами углов (О, О) и (200, 200), просто заменяется новым.
Операторы Imagel.Canvas.CopyMode:= cmSrcInvert; Imagel.Canvas.CopyRect(Rect(0, 0,200,200),Image2.Canvas, Rect(0,0, 200,200)); Imagel-Canvas.CopyRect{Rect{0,0,200,200),Image2-Canvas, Rect(0,0,200,200)); обеспечивают копирование изображения фрагмента канвы ком- понента Image2 в указанную область канвы компонента Imagel в режиме cmSrcInvert. После выполнения функции CopyRect в первый раз изображения в компонентах Imagel и Image2 нала- гаются друг из друга, а в результате выполнения функции Copy- Rect во второй раз исходное изображение на канве компонента Imagel восстанавливается. Операторы Imagel.Canvas.CopyMode:= cmWhitenes s; Imagel-Canvas-CopyRect{Rect{0,0,200,200),Image2.Canvas, Rect(0,0,200,200)),- просто очищают указанную область канвы компонента Imagel, закрашивая ее белым цветом. При этом изображение в компо- ненте Image2 никак не участвует в операциях копироваяия. Помимо копирования свойство CopyMode используется также методом рисования на канве Draw. Его описание: procedure DrawtX, Y: Integer; Graphic: TGraphic); Метод Draw рисует изображение, содаржащееся в объекте, указанном параметром Graphic, сохраняя исходный размер изоб- ражения в его источнике и перенося изображение в область кан- вы объекта, верхний левый угол которой определяется парамет- рами X и Y. Источник изображения может быть битовой матри- цей, пиктограммой или метафайлом. Если источник — объект типа TBitmap, то перенос изображения производится в режиме, установленном свойством канвы CopyMode. Например, оператор Imagel.Canvas.Draw(10,10, Bitmapl); рисует на канве компонента Imagel изображение из компонента Bitmapl в область с координатами левого верхнего угла (10,10). Еще один метод рисования — DrawFocusReet. Этот метод ри- сует изображение прямоугольника в виде, используемом для ото- бражения рамки фонуса, операцией X0R. Функция DrawFocus- Rect объявлена следующим образом: procedure DrawFocusReet(const Rect: TRect);
Гроф>жо и муиьтимедио 175 где Rect — прямоугольная область. Поскольку при рисовании используется операция XOR, то повторный вызов этого метода с тем же значением Rect удаляет изображение прямоугольника- 6.1.6 Продолжение создания собственного графического редактора Теперь мы рассмотрели большинство вопросов, связанных графикой. Можно вернуться к созданному нами ранее собствен ному графическому редактору (раздел 6-1.4) и попробовать его усовершенствовать. Код подобного усовершенствованного редак- тора вы можете найти в приложении П.2. Его вид после выполне- ния ряда операций и в момент перетаскивания фрагмента изоб- ражения приведен на рис. 6.12. Редактор этот чисто демонстра- ционный и поэтому в нем нет многого, что должно быть в настоя- щем графическом редакторе. К тому же и тот инструментарий, который в нем имеется, надо бы было реализовать компактнее и с большей надежностью. Но в данном случае функциональность принесена в жертву простоте и наглядности кода. Рис. 6.12 Графический редактор в момент пе- ретаскивания фрагмента изоброже- Мы не будем подробно анализировать текст этого приложе- ния. Рассмотрим только основные приемы, которые использова- ны при создании различных инструментов. А общую логику ра- боты вы можете придумать сами или позаимствовать ее из при- мера П.2.
т Глава 6 Приложение выполняет следующие функции: Установка основного и дополнительного цветов. Щелчок на панели цветов левой кнопкой мыши устанавливает основной цвет, а щелчок правой кнопкой — вспомогательный. Кисть — кнопка SBBrush. Закрашивает замкнутую область, ограниченную цветом того пикселя, который указан щелчком мьппи. При щелчке левой кнопкой закрашивание производит- ся основным цветом, при щелчке правой кнопкой — вспомога- тельным- Индикация цвета — кнопка SBColor. В этом режиме вы може- те указать курсором мьппи любой пиксель на изображении и, щелкнув левой кнопкой, установить цвет этого пикселя как основной, а щелкнув правой кнопкой, установить его как вспомогательный цвет. Карандаш — кнопка SBPen. В этом режиме вы можете рисо- вать произвольную кривую основным цветом. Выделение фрагмента — кнопка SBRect. Фрагмент выделяет- ся точечной рамкой. Выделенный фрагмент можно в дальней- шем перетащить мышью на другое место. Если в процессе пе- ретаскивания нажата клавиша Ctrl, то производится копиро- вание фрагмента, в противном случае — вырезание, при кото- ром область начального размещения фрагмента закрашивает- ся вспомогательным цветом. Выделенный фрагмент может быть также скопирован или вырезан в буфер обмена Clipboard соответствующими командами меню. Стирание изображения (ластик) — кнопка SBErase. Переме- щение ластика закрашивает область под ним во вспомогатель- ный цвет. Рисование прямоугольника — кнопка SBRectang. Рисуется прямоугольная рамка основным цветом. Рисование заполненного прямоугольника — кнопка SBFil- IRec. Рисуется прямоугольная рамка основным цветом и пря- моугольник внутри закрашивается вспомогательным цветом. Рисование прямой линии — кнопка SBLine. Рисуется прямая линия основным цветом. Открытие графического файла — команда Файл | Открыть. Сохранение изображения в графическом файле — команда Файл | Сохранить как... . Отмена операций, выполненных последним использованным инструментом — команда Провка | Отменить.
Графике и мультимедиа 177 Копирование или вырезание выделенного фрагмента изобра- жения в буфер обмена Clipboard — команды Правка | Копиро- вать или Правка | Вырезать. Вставка графического изображения типа битовой матрицы из буфера обмена Clipboard — команда Правка | Вставить. Начнем с рассмотрения функции выделения фрагмента. Она осуществляется методом DrawFocusRect. В этом режиме при со- бытии OnMouseDown холста — компонента Image3, выполняют- ся операторы: {запоминание начального положения курсора мыши} ХО:=Х; YO:=Y; (формирование начального положения области фрагмента} R.TopLeft:=Point(X, Y}; R.BottomRight:=₽oint(X, Y); {рисование рамки} Image3.Canvas.DrawFocusRect(R); RBegin:=true; Эти операторы запоминают координаты мыши X и Y в пере- менных ХО и Y0, задают начальные координаты прямоугольной области — переменной R типа TRect и рисуют рамку (пока нуле- вого размера) методом DrawFocusRect. Устанавливается также флаг начала выделения фрагмента — переменная RBegin. При событии OnMouseMove компонента Image3, если уста- новлен флаг RBegin, выполняются операторы: {стирание прежней рамки) ВпадеЗ.Canvas.DrawFocusRect(R); {формирование области R) if ХО<Х then begin R.Left:=X0; R.Right:=X end else begin R.Left:=X; R.Right:=XO end; if Y0<Y then begin R.Top:=YO; R.Bottom:=Y end else begin R.Top:=Y; R.Bottom:“YD end; {рисование новой рамки} Image3.Canvas.DrawFocusRect(R); Первый из этих операторов стирает прежнее изображение рамки (напомним, что метод DrawFocusRect рисует рамку с по- мощью операции XOR). Два следующих оператора формируют новую область R из начальных координат (ХО, Y0) и текущих ко- ординат курсора (X, Y). Дело в том, что область, передаваемая в функцию DrawFocusRect, должна быть сформирована ♦правиль- но» — значение R.Left должно быть меньше R.Right, a R.Top — меньше R.Bottom. А поскольку пользователь может перемещать курсор в любом направлении и соотношение координат (ХО, Y0)
jra Гла«а 6 и (X, Y) может быть любым, требуется упорядочивание коорди- нат. Последний оператор рисует рамку в новом положении. Итак, рамка, ограничивающая фрагмент нарисована. Теперь рассмотрим процедуру перетаскивания пользователем выделен- ного фрагмента. Если пользователь помещает курсор внутрь вы- деленной области и нажимает кнопку мыши, выполняются опе- раторы: with ХтадеЗ.Canvas do begin {стирание прежней рамки) DrawFocusReet(R); {установка флага перетаскивания) RDrag:=true; {запоминание начального положения курсора мыши} ХО:=Х; YO:=Y; {запоминание начального положения перетаскиваемого фрагмента) RO.TopLeft:=R.TopLeft; RO.BottomRight:=R. BattomRight; {запоминание изображения) Bi tMap.Assign(Image3-Picture); {установка цвета кисти) Brush.Color:=Image2.Canvas.Brush.Color; end; Первый оператор стирает рамку. Второй — устанавливает флаг- перетаскивания — переменную RDrag. Два следующих опе- ратора запоминают начальное положение перетаскиваемого фрагмента в переменной R0 типа TRect. Следующий оператор за- поминает методом Assign изображение в момент начала перетас- кивания в переменной BitMap. Это необходимо, чтобы в процессе перетаскивания можно было восстанавливать испорченные места изображения и чтобы при желании пользователя можно было в дальнейшем отменить результат перетаскивания. Последний оператор задает цвет кисти равным вспомогательному цвету, хранящемуся в компоненте Image2. При событии OnMouseMove компонента JLmage3, если уста- новлен флаг RDrag, выполняются операторы: {восстановление изображения под перетаскиваемым фрагментом) CopyRect(R,BitMap.Canvas, R) ; {если не нажата клавиша Ctrl — стирание изображения в НО) if not (ssCtrl in Shift) then FillRect(RO); (формирование нового положения фрагмента) R.Left:=R-Left+X-XO; R.Right:=R.Right+X-XO;
Графика и мультимедиа 179 R.Тор:=R.Тор+Y-YO; R.Bottom:=R.Bot tom+Y-YO; (запоминание положения курсора мыши} ХО:=Х; YO:=Y; {рисование фрагмента в новом положении} CopyRect{R,BitMap.Canvas,RO); {рисование рамки) DrawFocusRect{RJi Первый оператор восстанавливает изображение под перетас- киваемым фрагментом в его прежней позицией (т.е. стирает фрагмент), копируя соответствующую область методом CopyRect из компонента BitMap. Далее, если не нажата клавиша Ctrl, то очищается область начального размещения фрагмента (осущест- вляется вырезание) методом FillRect- Затем запоминаются но- вые координаты курсора и новое положение фрагмента, после чего фрагмент и его рамка рисуются в новом положении. Вот в общих чертах и все, что надо сделать для осуществления наиболее сложных операций — выделения фрагмента и его пере- таскивания. Режимы рисования заполненного и не заполненного прямо- угольников проще. Начало этих режимов по событию OnMouse- Down и их продолжение по событиям OnMouseMove не отлича- ются от рассмотренного ранее режима выделения фрагмента. От- личие только в том, что при завершении формирования пользо- вателем прямоугольной рамки, т.е. при событии OnMouseUp, надо в данном случае нарисовать прямоугольник. Рисование за- полненного прямоугольника осуществляется операторами: with Image3.Canvas do begin Brush-Color:=Image2.Canvas.Brush.Color; Pen.Color:=Imagel.Canvas.Brush.Color; Rectangle(R.Left,R.Top,R.Right,R.Bottom) i end; Они задают цвета кисти и пера и рисуют прямоугольник мето- дом Rectangle. Рисование не закрашенного прямоугольника осуществляется операторами: with ТшадеЗ.Canvas do begin Brush.Color:=Imagel.Canvas-Brush.Color; FrameRect(R); end; Обратите внимание, что равным основному цвету задается цвет кисти, а не пера, поскольку метод FrameRect рисует цветом кисти.
ISO Глава 6 Рисование прямой линии осуществляется следующим обра- зом. Начало рисования по событию OnMouseDown сводится к операторам: ХО:=Х,- YO:=Y; Х1:=Х; Yli=Y; Image3.Canvas.Pen.Color:=Imagel.Canvas.Brush.Color; Image3.Canvas.Pen.Mode;=pmNotXor; Они запоминают положение курсора в двух наборах перемен- ных: (XO.YO) и (Х1> Y1). Зачем нужны два набора — будет сказа- но позднее. Затем устанавливается цвет пера и режим pmNot- Хог, которым позволит при движении мыши стирать изображе- ние линии. При событиях OnMouseMove работают следующие операторы: with Image3.Canvas do begin {стирание прежней линии} MoveTo(X0,Y0); LineTo(XI,Yl); {рисование новой линии} MovaTo(XO,YO); LineTo{X,Y); {запоминание новых координат конца линии} Х1:=Х; Y1:=Y; end; В этих операциях сначала парой методов MoveTo и LineTo стирается линия в прежнем положении, а затем такой же парой методов рисуется новая линия. После этого запоминаются новые координаты конца линии. Попробуем разобраться, зачем все так сложно. Ведь казалось бы, что достаточно всего двух операторов: LineTo(ХО,У0>; LineTo(X,Y); Первый из них сотрет прежнюю линию, поскольку текущая позиция пера после рисования этой прежней линии соответство- вала концу линии (тем координатам, которые мы храним в пере- менных XI, Y1). А второй оператор нарисует новую линию. И не надо хранить никаких координат. Так просто нельзя сделать из-за одной тонкости: метод LineTo рисует линию, начинающуюся в текущей позиции пера и закан- чивающуюся в указанной точке, исключая ЭТУ конечную точку. Поэтому, если выполнить в режиме pmNotXor приведенные выше операторы, то первый из них сотрет прежнюю линию, но оставит нестертой точку с координатами (ХО, Y0) и кроме того
Графика и муиьтимедио нарисует точку в бывшей позиции курсора, поскольку она как конечная была исключена при рисовании предыдущей линии. Таким образом, на концах линии останется «грязь* в виде конце- вых точек, W эрая и будет тянуться за перемещающимся курсо- ром мыши. Заключительные операции при событии OnMonseUp анало- гичны рассмотренным выше, но дополняются переводом пера в режим ршСору, при котором рисуется окончательная линия: with ТшадеЗ.Canvas do begin (стирание прежней линии} MoveTo(ХО,YOJ; LineTo(XI,Y1); (рисование новой линии} Pen.Mode:=ptnCopy; MoveTo(XO,YO): LineTo(X,Y); end; Теперь рассмотрим инструмент Перо» позволяющий рисовать произвольные линии. Казалось бы естественной реализацией это- го инструмента был бы оператор ТшадеЗ.Canvas.Pixels(X,Y(:=Imagel.Canvas.Brush.Color; который окрашивает пиксель под курсором в основной цвет. Од- нако, попробуйте так реализовать Перо и ничего хорошего не увидите. Курсор мыши перемещается быстро и события OnMou- seMove происходят вовсе не при перемещении на соседний пик- сель. Поэтому ваша линия распадется на отдельные точки, тем более редкие, чем быстрее пользователь будет перемещать кур- сор. Линию, оставляемую курсором, следует рисовать тоже мето- дом LineTo, поместив в обработчик события OnMouseMove опе- ратор ТгаадеЗ.Canvas.LineTo(X, Y) Мы рассмотрели почти все инструменты, введенные в прило- жении рис. 6-12. Коротко перечислим оставшееся. Ластик реали- зуется методом FillRect, очищающим изображение под его рам- кой. Кисть, отмена команд и загрузка внешнего файла рассмат- ривались в разделе 6.1.4. Сохранение файла осуществляется с ис- пользованием компонента типа SavePictureDialog (или SaveDia- log, если вы работаете не с Delphi 5, а с более ранними версиями) оператором if SavePictureDialog1.Execute then begin BitMap.Assign(Image3.Picture); BitMap.SaveToFile(SavePictureDialog1.Filename); end;
132 Слово 6 Изображение с холста сохраняется в компоненте BitMap. из которого методом SaveToFile записывается в выбранный пользо- вателем файл. Команды меню Копировать и Вырезать осуществляются проце- дурой procedure TForml.MCopyClick(Sender: TObject); var BMCopy:TBitMap; begin {стирание рамки} Image3.Canvas.DrawFocusRect(R); {создание временного объекта BMCopy) BMCopy:=BitMap.Create; BMCopy.Width:=R.Right-R.Left; BMCopy.Height:=R.Bottom-R.Top; try {копирование фрагмента в BMCopy} BMCopy.Canvas.Copyrect{Rect(0,0, BMCopy.Width,BMCopy.Beight},Image3.Canvas,R) ; (восстановление рамки) Image3.Canvas-DrawFocusRect(R); (копирование в Clipboard} ClipBoard.Assign(BMCopy); if (Sender as TMenuItem).Name='MCut‘ then begin (вырезание) Image3.Canvas.Brush.Color:=clWh ite; Image3.Canvas.FiliRect(R); end; finally {освобождение памяти} BMCopy.Free; end; end; Копированию или вырезанию подлежит ранее выделенный пользователем объект, местоположение и размеры которого определяются переменной R. Поэтому сначала создается времен- ный объект типа TBitMap, в который переносится копируемый фрагмент. Затем этот объект копируется в Clipboard. Благодаря разделу finally память освобождается от временного объекта при любом исходе копирования: удачном или аварийном. Аналогично реализуется команда Вставить, копирующая изоб- ражение из буфера обмена Clipboard: procedure TForml.MPas teClick(Sender: TObj ect); var BMCopy:TBitMap; begin BMCopy:=Bi tMap.Create;
Графики и мультимедиа 183 try try BMCopy.LoadFromClipBoardForma t(cf_BitMap, ClipBoard.GetAsHandle(cf Bitmap), 0) ; Image3.Canvas.CopyRect(Rect( 0,0,BMCopy.Width,BMCopy.Height) , BMCopy.Canvas,Rect( 0,0,BMCopy.Width,BMCopy.Height)); finally BMCopy.Free; except On EXnvalidGraphic do ShowMessage('Ошибочный формат графики'); Чтение из Clipboard осуществляется методом LoadFromClip- BoardFormat. Предусмотрен перехват исключения ElnvalidG- raphic, если в Clipboard содержится не битовая матрица. Попробуйте реализовать описанный графический редактор или разберитесь подробнее в его работе, посмотрев его код в раз- деле П.2. Попробуйте также усовершенствовать редактор, доба- вив, в него, например, выбор ширины линий, рисование эллип- сов и т.д. 6.1.7 События OnPoint До сих пор мы рисовали в основном на канве компонента Ima- ge. Но канву имеет не только Image. Ее имеют и многие другие компоненты, например, формы. Все, что ранее вы рисовали на канве компонентов типа TImage, вы могли бы рисовать и на фор- ме. Кроме того есть специальный компонент PaintBox, имеющий канву и позволяющий рисовать на ней. Рисование на PaintBox вместо формы не имеет никаких преимуществ, кроме, может быть, некоторого облегчения в расположении одного или неско- льких рисунков в площади окна. При рисовании на канве формы или PaintBox надо учитывать некоторые особенности. Давайте сначала выясним на собствен- ном опыте, о чем идет речь. Откройте новое приложение, перенесите на него диалог Ореп- PictureDialog (или OpenDialog) кнопку и в обработчик щелчка на ней вставьте операторы: procedure TForml.ButtonlClick(Sender: TObject); var BitMap:TBitMap; if OpenPictureDialogl.Execute then begin BitMap:“TBitMap.Create;
184 Глава 6 try BitMap.LoadFromFile(OpenPictureDialogl.FileName}; Canvas.Draw(0,0,BitMap); finally BitMap.Free; end; end; Эти операторы обеспечивают загрузку выбранного пользовате- лем графического файла и отображение изображения непосредст- венно на канве формы (поскольку оператор Canvas.Draw отно- сится к канве формы, можно было бы ето уточнить, написав Forml.Canvas.Draw). Запустите приложение, выберите файл и вы увидите ито-нибуль вроде представленного на рис. 6.13 а. А теперь, не закрывая своего приложения, вернитесь в Delphi и, ничего там не делая опять перейдите в свое выполняющееся при- ложение. Если окно Редактора Кода, выступившее не первый план при вашем переходе в Delphi, целиком перекрыло окно ва- шего приложения, то вернувшись в него вы увидите, что картин- ка в окне исчезла. Если же вы опять загрузите в него картинку и сдвинете окно приложения так, чтобы окно Редактора Кода не могло целиком его закрыть, то, повторив эксперимент с перехо- дом в Delphi и обратно вы, возможно, увидите результат, подоб- ный представленному на рис. 6.13 б. Рис. 6.13 Демонстрация исходного изображения (о) и его стирания (б) при перекрытии его другим окном Вы видите, что если окно какого-то другого приложения пере- крывает на время окно вашего приложения, то изображение, на- рисованное на канве формы, портится. В компоненте Image этого не происходило, поскольку в классе TImage уже предусмотрены все необходимые действия, осуществляющие перерисовку испор- ченного изображения. А при рисовании на канве формы или дру- гих оконных компонентов эти меры должен принимать сам раз- работчик приложения.
Графики и мультимедиа 185 Если окно было перекрыто и изображение испортилось, опе- рационная система сообщает приложению, что в окружении что-то изменилось и что приложение должно предпринять соот- ветствующие действия. Как только требуется обновление окна, для него генерируется событие OnPaint. В обработчике этого со- бытия (в нашем случае события формы) нужно перерисовать иэобра жение. Перерисовка может производиться разными способами в зави- симости от приложения. В нашем примере можно было бы выне- сти объявление переменной BitMap за пределы приведенной выше процедуры, т.е. сделать эту переменную глобальной, и пе- ренести оператор BitMap-Free из этой процедуры в обработчик события формы OnDestroy. Тогда в течение всего времени выпол- нения вашего приложения вы будете иметь копию картинки в компоненте BitMap и вам достаточно ввести в обработчик собы- тия OnPaint формы всего один оператор: Canvas.Draw(0,0,BitMap); Сделайте это, и увидите, что изображение на форме не портит- ся при любых перекрытиях окон. Сделанный вами обработчик перерисовывает все изображе- ние, хотя, может быть, испорчена только часть его. При больших изображениях это может существенно замедлять перерисовку и вызывать неприятные зрительные эффекты. Перерисовку можно существенно ускорить, если перерисовывать только испорчен- ную область канвы. У канвы есть свойство ClipRect типа TRect, которое в момент обработки события OnPaint указывает область, которая подлежит перерисовке. Поэтому более экономным будет обработчик: if BitMaponil then Canvas.CopyRect( Canvas.ClipRect,BitMap.Canvas,Canvas.ClipRect): Оператор if используется в нем, чтобы избежать ошибочного обращения к BitMap пока графический файл еще не загружался и объект BitMap не создан. А перерисовывается только область ClipRect, которая испорчена. 6.2 Мультимедиа и анимация 6.2.1 Звук 6.2.1.1 Типы звуковых и мультимедиа файлов Так же, как существует множество рассмотренных нами фор- матов графических файлов, существует немало файлов звуковых
186 Глава 6 и мультимедиа. Мы коротко охарактеризуем обе эти группы фай- лов в рамках данного раздела» так как файлы мультимедиа часто содержат звуковую дорожку и было бы не очень правомерно го- воря о звуке не упомянуть звук в мультимедиа. Наиболее простым звуковым файлом является волновой файл .wav. В нем записано цифровое представление информации о волновой форме электрического сигнала, соответствующего каж- дому звуку. Волновой файл «не знает» вообще ничего о том, что такое звук и что он означает; поэтому для хранения звукового клипа приходится запоминать массу информации. Другим часто применяемым типом файлов-носителей являют- ся файлы цифрового интерфейса музыкальных инструментов {MIDI). Файлы .midi используются для хранения музыкальных фрагментов. В этих файлах звук хранится в виде данных о том, на каких инструментах исполняются определенные ноты и как долго оии звучат. Одним из главных преимуществ MIDI является то, что файлы получаются сравнительно небольшими. Файлы МПИ относятся к волновым файлам примерно так же, как мета- файлы — к файлам .bmp. В обоих случаях файлы первого типа «понимают», какие данные они представляют, а файлы второго типа хранят сырые данные, просто посылаемые на выходное устройство. Волновые и MIDI файлы могут хранить только авук или музы- ку. Для хранения видео информации разработан ряд форматов. Отметим среди них файлы AVI и MPEG. Большинство видеофай- лов поддерживают также хранение звуковой дорожки, так что звук воспроизводится синхронно с картинкой. Что собой представляет видеофайл, и как он работает? Чело- веческий мозг интерпретирует быструю последовательность изображений, незначительно отличающихся друг от друга, как движение. Каждое на этих изображений называется кадром. Каждый следующий кадр несколько отличен от предыдущего. Чтобы мозг воспринимал смену кадров как плавное движение, желательно воспроизводить около 30 кадров в секунду. Более вы- сокая частота не приводит к заметному росту качества, а более низкая производит впечатление мерцания экраяа. Если бы каждый кадр хранился в файле в виде битовой матри- цы экрана (а это несколько сотен килобайт), то потребовался бы огромный объем дисковой памяти. При такой простой схеме хра- нения на компакт-диск, например, можно было бы записать все- го 72 секунды видеофильма- Реально для хранения видеофиль- мов используется техника сжатия видеоданных.
Грофмко и мультимедиа 187 Если не углубляться в сложную математику методов сжатия, то суть сводится к следующему. Когда захватывается очередной кадр, аппаратура или программа сжатия задается вопросом: «Можно ли сохранить этот кадр более компактно, если записать только то, что в нем отличается от предыдущего, или нужно со- хранить картинку целиком?» Чаще всего выгодно сохранять то- лько изменившиеся части сцены. Но в определенных обстоятель- ствах, например, при переключении на другую камеру, наклад- ные расходы описания изменений заняли бы больше места, чем непосредственное сохранение кадра. В методах хранения мультимедиа достигнуты большие успе- хи. Сейчас можно записать целый полнометражный кинофильм на стандартном CD-ROM. 6.2.1.2 Процедуры воспроизведении звуков Веер, MessageBeep и PlaySound Наиболее простой процедурой, управляющей звуком, являет- ся процедура Веер. Она не имеет параметров и воспроизводит стандартный звуковой ситная, установленный в Windows, если компьютер имеет звуковую карту и стандартный сигнал задан (он устанавливается в программе Windows «Панель управления* после щелчка на пиктограмме Звук). Если звуковой карты нет или стандартный сигнал не установлен, звук воспроизводится че- рез динамик компьютера просто в виде короткого щелчка. Откройте новое приложение, введите в него кнопку, в обработ- чике щелчка которой напишите одно слово: Веер; Можете запустить приложение, щелкнуть на кнопке и про- слушать стандартный авук Windows или просто щелчок, если стандартный звук не установлен. Более серьезной процедурой является MessageBeep. Она опре- делена как function MessageBeep(uType:word)ibcolean; Параметр пТуре указывает воспроизводимый звук как иден- тификатор раздела [Bounds] реестра, в котором записаны звуки, сопровождающие те или иные события в Windows. С помощью приложения Звук в «Контрольной пенели» пользователь может удалить или установит соответствующие звуки. Параметр uType может иметь следующие значения:
IM Глава 6 Значение Звук 1 MBJCONASTERISK SystemAsterisk — звездочка MBJCONEXCLAMATION MB_ICONHAND SystemExclamation — восклицание SystemHand — критическая ошибка MBICONQUESTTON SystemQuestion — вопрос МВ_ОК SystemDefault -— стандартный звук После запроса звука функция MessageBeep возвращает управ- ление вызвавшей функции и воспроизводит звук асинхронно. Во время воспроизведения приложение может продолжать выпол- няться. Если невозможно воспроизвести указанный в функции звук, делается попытка воспроизвести стандартный системный звук, установленный по умолчанию. Если и это невозможно, то воспро- изводится стандартный сигнал через динамик. При успешном выполнении возвращается ненулевое значе- ние. При аварийном завершении возвращается нуль. Можете в своем тестовом приложении ввести еще одну кнопку и написать для нее обработчик: MessageBeep(МВ_ОК); Вы услышите тот же стандартный звук Windows, что и при выполнении процедуры Веер. Или услышите тот же тихий щел- чок, если стандартный звук не установлен. Попробуйте устано- вить различные звуки с помощью «Панели управления» и прове- рить MessageBeep при различных значениях ее параметра. А теперь давайте займемся более серьезной функцией PlaySo- und, которая позволяет воспроизводить не только звуки событий Windows, но и любые волновые файлы- Это функция API Win- dows, параметры которой описаны в модуле mmsystem. Поэтому для использования этой функции в вашем приложении необхо- димо включить в его оператор uses ссылку на mmsystem, поско- льку автоматически эту ссылку Delphi не включает. Функция PlaySonnd определена следующим образом: function PlaySound(pszSound:PChar; hmodiHINST; fdwSound:Cardinal) :boolean; Параметр pszSound представляет собой строку с нулевым символом в конце и определяет воспроизводимый звук. Пара- метр hmod используется, если звук берется из ресурса. Посколь- ку далее звуками из ресурса мы пользоваться не будем, то sounds всегда можно задавать равным nil или О.
Графика и мультимедиа 189 Параметр fdwSound является множеством флагов, которые определяют режим воспроизведения и тип источника звука. Ниже приведены некоторые из этих флагов, наиболее важные для воспроизведения произвольных волновых файлов: SND_ASYNC Звук воспроизводится асинхронно и функция Play- Sound возвращается немедленно после начала вос- произведения. Чтобы прекратить асинхронное вос- произведение волнового файла, надо вызвать Play- Sound с параметром pszSound, равным 0. SND. .LOOP Воспроизведение звука постоянно повторяется, пока не вызовется PlaySound с параметром pszSo- und, равным 0. Одновременно надо установить флаг SND_ASYNC асинхронного воспроизведения звуна. SND_NOSTOP Если заданный звук не может быть воспроизведен, поскольку ресурсы, необходимые для воспроизведе- ния, заняты воспроизведением другого звука, фун- кция PlaySoHnd немедленно вернет false, Ее вос- производя заданного звука. Если данный флаг не указан, функция PlaySound пытается остановить воспроизведение другого звука, чтобы устройство могло быть использовав© для воспроизведения но- вого звука. SND. .NOWAIT Если драйвер занят, функция сразу вернется без воспроизведения заданного звука. SND_PURGE выпаянных в данной задаче. Если pszSound не 0, останавливаются все экземпляры указанного звука. Если pszSound равен 0, то останавливаются все звуки, связанные с данной задачей- SND. .SYNC Синхронное воспроизведение звука события. Функ- ция PlaySound возвращается только после оконча- ния воспроизведения. Флаги могут комбинироваться операцией or. Звук, указанный параметром pszSound, должен помещаться в доступную память и должен подходить для установленного драй вера устройства воспроизведения волновых файлов. Функция PlaySound ищет файл звука в следующих каталогах: текущем, каталоге Windows, системном каталоге Windows, каталогах, пе- речисленных в переменной среды PATH, в списке каталогов, пре- доставляемых сетью. Если указанный звук не находится, функ- ция PlaySound воспроизводит системный звук по умолчанию.
190 Глад» 6 Если функция не может найти и его, то воспроизведения не бу- дет, а вернется значение false Приведем примеры использования функции PlaySound. Оператор PlaySound( 'с:\win9 5\media\3вук Microsoft.wav1,0,SND_ASYNC); воспроизводит асинхронно и однократно стандартный звук Mic- rosoft, который вы обычно можете слышать при открытии Win- dows. В процессе воспроневедения продолжается выполнение приложения. Чтобы опробовать функцию PlaySound, введите в свое прило- жение диалог OpenDialog и кнопку со следующим обработчиком щелчка: var Pchs array[О..128) of char; begin if OpenDialogl.Execute then begin StrPCopy(Pch,OpenDialogl.FileName); PlaySound(Pch,0,SND_AS¥NC); end; end; Запустите приложение, выберите файл какой-нибудь прият- ной музыки и работайте со своим приложением, наслаждаясь по- путно выбранной мелодией. В предыдущих примерах звук задавался именем его волнового файла. Функция PlaySound позволяет воспроизводить и систем- ные звуки, просто называя их псевдонимы. Например, оператор PlaySound(’SystemStart',0,SND_ASYNC>; воспроизведет тот же звук открытия Windows, что и приведен- ный ранее оператор, указывавший имя и путь к нему. Оператор PlaySound(* С:\win95\media\3ByK Microsoft.wav1, 0, SND_AS¥NC or SND_LOOP); многократно асинхронно воспроизводит стандартный звук Micro- soft, начиная его снова и снова, как только ои заканчивается. Если вы ввели в свое приложение подобный оператор (пусть даже и с очень приятной музыкой), вам надо предусмотреть еще и ка- кую-нибудь кнопку, по которой воспроизведение прерывается за- данием нового авука или выполнением оператора PlaySound(0,0, SND_PURGE) ; Оператор PlaySound( 1 с:\win95\media\3вук Microsoft.wav *,0,SNDjSYNC) ;
Графика и мультимедиа 191 будет воспроизводить звук синхронно. Т.е. функция PlaySound не вернется, пока воспроизведение не завершится. На это время ваше приложение будет блокировано. Впрочем, ваши действия во время этого вынужденного простоя запомнятся системой. И если вы, слушая музыку, щелкнули на той же кнопке повторно, то после окончания звука он будет повторен, так как ваш щелчок встал в очередь событий. Все рассмотренные ранее операторы прерывали при своем вы- полнении звук, который асинхронно воспроизводился в момент вызова PlaySound. Если же вы выполните оператор с флагом SND_NOSTOP, например PlaySound(1с:\win95\media\Звук Microsof t.wav',О, SNDSYNC or SND_NOSTOP} то в случае, если в этот момент драйвер занял воспроизведением другого звука, это воспроизведение не будет прерываться, а фун- кция PlaySound сразу вернет false. Впрочем, заказанного этим оператором звука вы в этом случае не услышите, т.к. в очередь он не встанет. Мы рассмотрели основные функции воспроизведения звука. В Delphi (начиная с Delphi 2) имеется также компонент MediaPlay- ег — универсальный проигрыватель аудио- и видео-информации. Он будет рассмотрен в разделе 6.2.4. 6.2,2 Начала анимации — создание собственной мультипликации А теперь давайте займемся движущимися изображениями. Но прежде, чем учиться просматривать готовые видеофайлы, попро- буем свои силы в создании собственной анимации. Каждый примерно представляет себе принципы создания мультипликационных фильмов, знает, что они представляют со- бой совокупность множества кадров, каждый из которых чуть-чуть отличается от предыдущего. Это при быстром пооче- редном просмотре кадров и создает иллюзию движения. Вам, ко- нечно, в своей работе не придется рисовать с помощью Delphi мультфильмы. Для этого имеются совершенно другие инстру- менты. Но некоторые простенькие анимации — оживление изоб- ражений, иногда желательно делать. Например, при создании какой-нибудь обучающей программы может захотеться оживить какие-то схемы или условные изображения механизмов, чтобы показать в движении взаимодействие их отдельных составляю- щих. Или применить анимации типа тех, которые используются в программе Windows «Проводник» при копировании и удалении файлов.
192 Давайте попробуем сделать простую мультипликацию. Но, чтобы ие связываться с изображениями каких-то механизмов, сделаем нечто всем понятное: например условное изображение человечка, шагающего и бьющего при этом в литавры. Откройте новое приложение. Перенесите на форму компонен- ты Image, кнопку Button и таймер Timer. Кнопку разместите внизу формы. Основную площадь формы должен занимать ком- понент Image, на котором и будет рисоваться изображение (рис. 6.14). Приложение с простой мультипликацией Таймер будет задавать темп смены кадров. Поскольку в пер- вом варианте приложении у нас будет всего два кадра, задайте значение свойства Interval таймера достаточно большим, напри- мер, 500 (поскольку интервалы задаются в миллисекундах, то это значение соответствует 0,5 сек). Значение параметра Enabled таймера установите в false. Таймер у нас будет управляться кнопкой. Теперь размещение компонентов закончено. Надо ввести текст программы. Ее раздел implementation имеет следующий вид: const num:word=0; Н=20; // шаг Xpos:word=2*H; // координата туловища Ypos=120; // "земля” Ншеп=30; // высота тела Rhead=10; // радиус головы Rhead2=Rhead div 2; // радиус литавров revers:integer=l; // направление движения L=trunc(Н*1.41); // длина ноги Procedure Drawl var Yheadiword; // координата низа головы with Forml.Imagel.Canvas do begin
Графика и мультимедиа 193 case num of О: begin Yhead:=Ypos-H-Hmen; MoveTo(Xpos-H, Ypos) ; LineTo(Xpos,Ypos-H}; 11 нога LineTo(Xpos+H,Ypos); // другая нога MoveTo(Xpos,Ypos-H); LineTo(Xpos,Yhead); // туловище MoveTo(Xpos+revers*H,Yhead-H) ; LineTo(Xpos,Yhead+4); // рука Ellipse(Xpos+revers*H-Rhead2,Yhead-H-Rhead2, Xpos+revers*H+Rhead2,Yhead-H+Rhead2); LineTo(Xpos+revers*H,Yhead+H); // другая рука Ellipse{Xpos+revers*H-Rhead2,Yhead+H-Rhead2, Xpos+reve r s * H+Rhead2,Yhead+H+Rhead2) ; Ellipse(Xpos-Rhead,Yhead, Xpos+Rbead,Yhead-2*Rbead); Rectangle(Xpos-Rhead,Yhead-2*Rhead-l, Xpos+Rhead,Yhead-2*Rhead-4); l! шляпа end; 1: begin Yhead:“Ypos-L-Hmen; MoveTo(Xpos,Ypos); LineTo(Xpos,Yhead); MoveTo(Xpos,Yhead+4); LineTo(Xpos+revers*L,Yhead+4); Ellipse(Xpos+revers*L-Rhead2,Yhead+4-Rhead2, Xpos+revers*L+Rhead2,Yhead+4+Rhead2); Ellipse{Xpos-Rhead,Yhead, Xpos+Rhead, Yhead-2*Rhead); Rectangle(Xpos-H div 2,Yhead-2*Rhead-l, Xpos+H div 2, Yhead-2*Rhead-4); end; end; end; end; procedure TForml.BRunClick(Sender: TObject); begin Timerl.Enabled:= not Timerl.Enabled; procedure TForml.TimerITimer(Sender: TObject); begin Drawl; if (Xpos>=Imagel.Width-H)or(Xpos<=H) then revers:=-revers; Xpos:=Xpos+revers*H; num;=1-num; Drawl; end;
procedure TForml.Formereate(Sender: TObject); begin with Imagel.Canvas do begin MoveTo{0,Ypos+3); Pen.Width:=4; LineTo(Clientwidth,Ypos+3); Pen.Width:=1; Pen.Mode t =pmNotXor; end; Drawl; end; Начнем анализ этого кода с конца — с последней процедуры TForml.ForinCreate, являющейся обработчиком события ОпСге- ate формы. В этой процедуре рисуется линия, отображающая «землю», по которой будет ходить наш человечек. Затем устанав- ливается режим пера pmNotXor. И в заключение вызывается процедура Drawl, которая рисует исходное положение человеч- ка. Процедура TForml,BRunClick является обработчиком собы- тия OnClick кнопки. Каждый щелчок на кнопке включает или выключает таймер, в результате чего человечен идет или оста- навливается. Процедура TForml-TimerlTimer является обработчиком со- бытия OnTimer таймера. События OnTimer наступают с перио- дичностью, заданной свойством Interval. При возникновении этого события нам надо стереть прежний кадр н нарисовать но- вый. Сначала вызывается процедура Drawl. Поскольку позиция человечка с момента показа предыдущего кадра не изменилась, то этот вызов рисует по тому же самому месту, по которому рисо- вался предыдущий кадр. Следовательно, предыдущий рисунок стирается. Затем анализируется позиция человечка Xpos. Если эта позиция отстоит от какого-либо конца холста Imagel на ве- личину, меньшую шага Н, то изменяется на обратный знак пере- менной revers, характеризующей направление движения. Если revers = 1, человечек шагает вправо; если revers = -1, человечек шагает влево. Затем позиция Xpos изменяется на величину ге- vers*H, т.е. на шаг вправо или влево. Изменяется переменная num, которая указывает номер высвечиваемого кадра: 0 или 1. В заключение вызывается процедура Drawl, которая рисует ука- занный кадр в указанной позиции. Последняя процедура, которую мы рассмотрим — процедура Drawl, рисующая кадр. Она достаточно длинная, но в ией нет ничего сложного. В зависимости от значения num рисуется один или другой кадр, причем в рисунке учитывается позиция -Xpos и направление движения revers.
Гро фико и мультимедиа 195 Сохраните ваше приложение и выполните его. Щелкнув на кнопке вы можете заставить вашего человечка перемещаться. Достигнув края формы он будет поворачиваться и шагать в об- ратном направлении. При вторичном щелчке на кнопке он будет останавливаться. Конечно, пока движения нашего человечка очень неуклюжи. Чуть позже мы научим его двигаться более плавно. А пока обсу- дим некоторые проблемы, связанные с построением даже просте- ньких мультипликаций. Первая из них — создание фона. Наш человечек движется в пустом пространстве и мы не замечаем этой проблемы. Но попро- буйте вставить в программу какой-нибудь фон. Например, вста- вьте в процедуру TForml.FormCreate после оператора with и пе- ред оператором MoveTo следующие операторы, рисующие чер- ный прямоугольник: Brush.Color:=0; Rectangle(90,0,200,100) ; Brush.Color:=clWhite; Выполните теперь свое приложение. Во время движения чело- вечка вы увидите картину, приведенную не рис. 6.15. На черном фоне черный человечек становится белым. Если мультипликация черло-белая, то такой результат может только радовать, посколь- ку черное на черном просто исчезало бы. Но при разноцветном пе- стром фоне картина становится безрадостной. Вы можете в этом убедиться, если введете в свое приложение компонент OpenDialog и в начале процедуры TForml.FormCreate вставите оператор if Орелdialog!.Execute then Imagel.Picture.LoadFromFlle(Opendialcgl.filename); который позволит вам перед началом работы приложения загру- зить в виде фоне какой-нибудь графический файл, например, изображение земного шара, использованное ранее на рис. 6.1 — 6.3. Тогда вы увидите, что на пестром фойе земного шара ваш че- ловечек будет совершенно теряться. Рис. 6.15 Изменение цвета мультипликации при на- ложении на однотонный фон
186 Глава 6 Какие возможны выходы ив положения? Наиболее простой — ограничиваться черио-белой мультипликацией или. во всяком случае, ие использовать пестрых фонов. Если же по какой-то причине это невозможно, то, вероятнее всего, вам придется отка- заться от режима пера pmNotXor и использовать буферизацию фона. В нашем примере это можно было бы сделать следующим об- разом. Откройте свой предыдущий проект мультипликации и со- храните его под новым именем командой File | Save Project As. За- тем сохраните под новым именем файл модуля командой File | Save As. Теперь вы можете вводить в модуль изменения, не опасаясь испортить свой предыдущий проект. Введите глобальную переменную типа TBitMap: var BitMapcTBitMap; Это будет объект, в котором вы будете сохранять фрагменты фоня, испорченные очередным кадром. Переделайте процедуру TForml,FormCreate следующим образом: procedure TForml.FormCreate(Sender: TObjectJ; begin BitMap:-TBitMap.Create; BitMap.Width:=2*(L+Rhead); BitMap.Height:=L+Hmen+2*Rhead+6; with Imagel.Canvas do begin MoveTo(C,Ypos+3); Pen.Width:=4; LineTo(Clientwidth,Ypos+3); Pen.Width:-2f end; BitMap.Canvas.CopyRecr(Rect(0,0,BitMap.Width, BitMap.Height}, Imagel.Canvas, Rect( Xpos-L-Rhead, Ypos-( L+Hmen+2*Rhead+5) , Xpos+L+Rhead,Ypos+1)}; Drawl; end; Первыми операторами этой процедуры вы создаете объект Bit- Map и задаете его размеры равными максимальным размерам изображения человечка. В конце процедуры, перед вызовом Drawl в компонент BitMap методом CopyRect копируется фраг- мент изображения, внутри которого будет расположен рисунок человечка. После этого процедурой Drawl рисуется соответству- ющий кадр. Обратите внимание, что в данном приложении от- сутствует оператор, задававший ранее режим пера pmNotXor. Так что по умолчанию рисунок будет делаться обычным образом.
Графика и мультимедиа 197 Поскольку приложение создало объект BitMap, надо не за- быть добавить в него обработчик события OnDestroy формы, в который вставить оператор BitMap.Free; Процедуру TForml.TiinerlTimer измените следующим обра- зом: procedure TForml.TimerlTimer(Sender: TObject); begin Imagel.Canvas.Draw(Xpos-L-Rhead,Ypos-L~ Htnen-2*Rhead-5, BitMap); if (Xpos>=Iniagel.Width-H)or(XpcsOH) then revers:=-revers; Xpos:=Xpos+revers*H; num:=l-num; BitMap.Canvas.CopyRect(Rect( 0, 0,BitMap.Width,BitMap.Height), Imagel.Canvas,Rect( Xpos-L-Rhead, Ypos-(L+Hmen+2*Rhead+5) , Xpos+L+Rhead,Ypos+1)); Drawl; end; Если вы сравните с тем, что было в предыдущем приложении, то увидите, что вместо первого вызова процедуры Drawl, кото- рый стирал предыдущий кадр, вводится оператор Ixnagel.Can- vas.Draw, который выполняет ту же функцию, ио путем восста- новления запомненного ранее фрагмента фона под рисунком. Вторым отличием является наличие оператора BitMap.Can- vas.CopyRect, который перед вызовом Drawl запоминает новый фрагмент фона. Вот и все изменения. Выполните проект. Вы увидите, что без фона приложение работает как и прежде. Добавьте фон так же, как делали это раньше. Вы сможете увидеть, что изображение стало несколько лучше, ио не на много. Так что, как говорилось выше, все равно использовать для мультипликаций пестрые фоны крайне нежелательно. Наше изображение было очень простым и рисовалось быстро. Но при сложных изображениях время рисования может быть за- метным и приводить к мерцанию картинки и другим неприят- ным зрительным эффектам. В этих случаях используют буфери- зацию изображения. Это напоминает то, что вы только что дела- ли с фоном, ио относится не к фону, а к рисунку. Рисование оче- редного кадра производится не сразу на холсте, который видит пользователь, а на канве невидимого компонента, типа того Bit- Map, с которым вы только что работали. А после того, как рису- нок сделан, он переносится на видимый холст методами Draw
или CopyRect. Эти методы работают очень быстро и никаких неприятных мерцаний ие происходит. Еще одна проблема анимации — определение того, какие эле- манты изображаемого объекта видны, а какие — нет. Несмотря на простоту нашего примера с человечком, даже в нем возникла такая проблема, но мы ею пренебрегли, чтобы не усложнять код. Если вы внимательно посмотрите на рис. 6.14 или 6.15, то увиди- те, что изображение неправильное. Конец одной из рук должен быть скрыт за литаврами, которые держит человечек. Эту ошиб- ку в данном случае не трудно было бы убрать, но код несколько усложнился бы. А вот в трехмерной графике при вращении изоб- ражения объекта подобная проблема встает очень остро и должна соответствующим образом решаться. И последний вопрос, который мы рассмотрим, — как сделать нашу мультипликацию более плавной. Если вы не стремитесь к лаврам Диснея, вам достаточно ограничиться в ваших мульти- пликациях простыми механическими движениями, которые все- гда можно описать соответствующими функциями. Это относит- ся к любым динамическим иллюстрациям работы механизмов, к любым схематическим перемещениям. Можно применять функ- циональное описание движения и к нашему человечку. Вполне допустимо считать, что его руки и ноги движутся по окружно- стям с соответствующими центрами. Тогда легко рассчитать их положение в любой момент времени и соответственно разбить это движение на любое число кадров. Давайте сделаем движения нашего человечка более плавны- ми. Откройте опять ваше первое приложение с анимацией и опять сохраните модуль и сам проект под новыми именами. Вы можете ничего не добавлять на форму, а только изменить тексты процедур. Теперь эти тексты должны иметь следующий вид. // длина ноги и руки // координата опорной ноги // "земля” // высота тела // радиус головы // направление движения // число кадров на таг // иомер кадра const Н=ЗО; Xpos:word=3* Н; Ypos=120; Hmen=30; Rhead=lD; revers:integer-1; Ncadx=16; cadr:word=0; Procedure Drawl; var Yb,Yt,X:word; // координаты низа и верха туловища XI:word; Anglrreal; // угол with Forml.Imagel.Canvas do
Грсфикд и мультимедиа 199 begin Angl:=Pi/4*(l+2*cadr/(Ncadr-1)); Yb:=trunc(Ypos-H*sin(Angl)); Yt:=Yb-Hmen; X:=trunc(Xpos-revers*H*cos(Angl)); MoveTo(X-(Xpos—X),Ypos); LineTo(X,Yb); It нога if cadrONcadr div 2—1 then LineTo(Xpos,Ypos); // другая нога MoveTo(X,Yb); LineTo(X,Yt); It туловище Xl:=X-revers*(Yb-Ypos); Moveto(XI,Yt+5-(Xpos-X)); Ellipse(Xl-Rhead div 2,Yt+5-(Xpos-X)-Rhead div 2, Xl+Rhead div 2,Yt+5-(Xpos-X)+Rhead div 2); LineTo (X,Yt+5); // рука if cadrONcadr div 2-1 then begin Ellipse(Xl-Rhead div 2,Yt+5+(Xpos-X)-Rhead div 2, Xl+Rhead div 2,Yt+5+(Xpos-X)+Rhead div 2); LineTo(Xl,Yt+5+(Xpos-X)); // другая рука end; Ellipse (X-Rhead, Yt-2*Rhead,X+Rhead, Yt) ; Rectangle(X-Rhead,Yt-2*Rhead-4, X+Rhead,Yt-2*Rhead-l) ; // шляпа end; end; procedure TForml.BRunClickfSender; TObject); begin Timerl.Enabled:= not Timerl.Enabled; end; procedure TForml.TimerITimer(Sender: TObject); begin Drawl; cadri=(cadr+l) mod Ncadr; if cadr=O then i f (Xpos<Imagel.Width-revers *3*H)a nd(Xpos>-revers* 3*H) then Xpos:=trunc{Xpos+revers*H*l-41) else revers:=-revers; Drawl; end; procedure TForml.FormCreate(Sender: TObject); with Imagel.Canvas do begin MoveTo(0,Ypos+3); Pen.Width:=4; LineTo{Clientwidth,Ypos+3); Pen.Width:=1;
200 Глава 6 Рел.Mode:=praNotXor; end; Tuner1.Intervalг=600 div Ncadr; Drawl; end; Процедура TForml.FormCreate отличается от того, что было в первом приложении, только оператором, задающим выдержку таймера (свойство Interval) путем деления 600 на константу Ncadr, которая задает число кадров на один цикл движения — на один шаг человечка. Как видно, длительность одного шага вы- брана равной 600 миллисекунд. Вы, конечно, можете изменить это значение, как и значение Ncadr, выбранное равным 16. Процедура TForml.BRunCiick не отличается от той, что была в первом приложении- Процедура TForml.TimerlTimer, как и в первом приложении начинается и кончается вызовами Drawl, первый из которых стирает иеображение предыдущего кадра, а второй — рисует новый кадр. После первого вызова Drawl рас- считывается значение переменной cadr оператором cadr: = (cadr+l) mod Ncadr,- Поскольку тут используется операция вычисления остатка от деления cadr+1 на Ncadr, то значение cadr последовательно по- лучает значения 0, 1, 2..Ncadr-1, О, 1, ... . Шаг начинается с cadr = 0. При этом проверяется, не приблизился ли человечек к краю формы, и если приблизился — изменяется направление движения (знак переменной revets). Наиболее серьезно изменилась процедура Drawl. Она начина- ется с определения угла наклона ног и рук Angl, исходя из номе- ра кадра. Затем на основе этого угла строится изображение, по- добное тому, которое было в первом приложении. Обратите вни- мание на то, что вторая нога и вторая руке рисуются только, если выполняется условие cadroNcadr div 2-1. Это связано с тем, что, если число кадров Ncadr четное, то в этот момент одна нога накладывается на другую и руки также накладываются друг на друга. Поскольку рисование идет в режиме pmNotXor, то это на- ложение приведет к тому, что у человечка вообще исчезнут в этом кадре руки и иоги. Правда, всего на один кадр, ио все равно неприятно. Ваше приложение готово. Можете сохранить его и выполнить. Вы увидите, что движения человечка стали плавными. 6.2.3 Воспроизведение немых видео клипов — компонент Animate Теперь рассмотрим способ воспроизведения в приложении Delphi стандартных мультипликаций Delphi и файлов .avi —
Графики и мультимедиа 201 клипов без звукового сопровождения. Это позволяет сделать ком- понент Animate, расположенный на странице Win32 библиотеки. Компонент Animate позволяет воспроизводить на форме стан- дартные видео клипы Windows (типа копирования файлов, поис- ка файлов и т.п.) и немые видео файлы .avi — Audio Video Inter- leaved. Эти файлы представляют собой последовательность кад- ров битовых матриц. Они могут содержать и звуковую дорожку, но компонент Animate воспроизводит только немые клипы AVI. TAnimate может воспроизводить клипы AVI из ресурсов, из файлов или из библиотеки Shell32.dll. если приложение работа- ет с Windows 95/98 или NT. Откройте новое приложение, перенесите на форму компонент Animate и познакомьтесь с ним. Воспроизводимое им изображение задается одним из двух свойств: FileName или CommonAVI. Первое из этих свойств, как ясно из его названия, позволяет в процессе проектирования или программно задать имя воспроизводимого файла. А свойство CommonAVI позволяэт воспроизводить стандартные мультипли- кации Windows. Это свойство объявлено следующим образом: type TCommonAVI = (aviNons, aviFindFolder, aviFindFile, aviFindComputer, aviCopyFlles, aviCopyFile, aviRecycleFile, aviEmptyRecycle, aviDeleteFile); property CommonAVI: TCommonAVI; Тип TCommonAVI определяет множество предопределенных в Windows мультипликаций типа копирования файлов, поиска файлов, удаления файлов и т.п. Что означает каждое значение вы увидите из тестового приложения, которое построите чуть по- зже. А пока установите значение CommonAVI, например, равным aviCopyFile. Это соответствует стандартному изображению копи- рования файла. Соответствующий начальный рисунок немедлен- но понеится на вашей форме. Свойство Repetitions компонента Animate задает число повторений воспроизведения клипа. Если оно равно О (значение по умолчанию), то воспроизведение повто- ряется вновь и вновь до тех пор, пока не будет выполнен метод Stop. При выполнении этого метода генерируется событие OnS- top, которое можно использовать, например, чтобы стереть изоб- ражение — сделать его невидимым. Если же свойство Repetitions задать большим нуля, оио опре- делит число повторений клипа. Задайте его, например, равным 3. А теперь установите свойство Active компонента Animate в true. Вы увидите (рис. 6.16), что еще в процессе проектирования
Глоап 6 ваше приложение заработает. Изображение оживет и клип будет повторен 3 раза. Анимация копирования файла Вы можете посмотреть воспроизводимое изображение по кад- рам. Для этого щелкните на компоненте правой кнопкой мыши и из всплывшего меню выберите разделы Next Frame (следующий кадр) или Previous Frame (предыдущий кадр). Это позволит вам выбрать фрагмент клипа, если вы не хотите воспроизводить клип полностью. Воспроизвести фрагмент клипа можно, установив со- ответствующие значении свойств StartFrame — начальный кадр воспроизведения, и StopFrame — последний кадр воспроизведе- ния. Воспроизводить фрагмент клипа можно и методом Play, кото- рый определен следующим образом: procedure Play(FromFrame, ToFrame: Word; Count: IntegerJ; Метод воспроизводит заданную последовательность кадров клипа от FromFrame до ToFrame включительно и воспроизведе- ние повторяется Count раз. Если FromFrame = 1, то воспроизве- дение начинается с первого кадра. Значение ToFrame должно быть не меньше FromFrame и не больше значения, определяемо- го свойством FrameCount (свойство только для чтения), указыва- ющим полное число кадров в клипе. Если Count = О, то воспроиз- ведение повторяется до тех пор, пока не будет выполнен метод Stop. Выполнение Play идентично заданию StartFrame равным FromFrame, StopFrame равным ToFrame, Repetitious равным Count и последующей установке Active в true. В компоненте Animate предусмотрены события OnClose, ОпО- pen, OnStart и OnStop, генерируемые соответственно в моменты закрытия и открытия компонента, начала и окончания воспроиз- ведения. Давайте теперь построим тестовое приложение, показываю- щее возможности компонента Animate. Установите в том прило- жении, которое вы уже начали, свойство Visible компонента Animate в false. Это надо для того, чтобы изображение возника- ло только тогда, когда произойдет соответствующее событие: но-
Графимо и мультимедиа 203 пирование файлов, поиск файлов и т.п. В тестовом приложении мы будем имитировать начало и окончание события, которое должно сопровождаться мультипликацией, нажатиями кнопок запуска и останова воспроизведения. Поэтому верните значение свойства Repetitions в О, чтобы воспроизведение длилось до окончания события. Свойство Active установите в false. Полезно также установить свойство AutoSize в false, а свойство Center в true, чтобы изображение всегда появлялось в центре экрана. А теперь добавьте в приложение 3 кнопки (рис. 6.17). Первая из них (назовите ее BWind) будет начинать процесс воспроизве- дения поочередно всех стандартных клипов Windows. Вторая кнопка (назовите ее BStop) пусть завершает воспроизведение очередного клипа. А третью кнопку (назовите ее BFile) введем для того, чтобы показать, что компонент может воспроизводить изображения из заданного файла .avi. Чтобы пользователь мог выбрать файл изображения, добавьте на форму компонент Open- Dialog и задайте его фильтр (свойство Filter) равным видео *.avi *.avi Рис. 6.17 Демонстроция возможностей компонента Animate Теперь все приготовления закончены и осталось только напи- сать обработчики событий. Код обработчиков может иметь вид: var ±:word; procedure TForml.BWindClickfSender: TObject); begxn Animatel.Visible:=true; Animatel-CommonAVI:=aviFindFolder; Animatel.Active:=true; procedure TForml.BStopClxek(Sender: TObject); Animatel.Stop; end; procedure TForml-BFileClick(Sender: TObject);
if OpenDialogl.Execute then with Animatel do begin i:=9; FileName:=OpenDialogl.FileName; Visible:=true; Active:=true; end; end; procedure TForml.AnimatelStop(Sender: TObject); begin Inc (i); with Animatel do begin case i of 2: CommonAVI:=aviFindFile; 3: CommonAVI:=aviFindComputer; 4: CommonAVI:=aviCopyFiles; 5: CommonAVI:=aviCopyFile ; 6: CommonAVI:=aviRecycleFile; 7 i CommonAVI:=aviEmptyRecycle; 8: CommonAVI:=aviDeleteFile; end; if i<9 then Active:=true else Visible:=false; end; end; Обработчик события OnClick кнопки В Wind задает начальное значение свойства CommonAVI, сбрасывает счетчик на 1, делает компонент Animatel видимым и активизирует его. Обработчик события OnClick кнопки BStop останавливает воспроизведение методом Stop. Обработчик события OnStop компонента Animatel увеличи- вает счетчик на 1, в зависимости от значения счетчика загружает в компонент соответствующий клип Windows, и активизирует компонент. Если все клипы уже воспроизведены, то компонент делается невидимым. Обработчик события OnClick кнопки BFile загружает в компо- нент видео файл, выбранный пользователем. Выполните приложение и проверьте его в работе. В качестве видео файла можете использовать файл ...\Demos\Coolstuf\Cool.ovi, поставляемый с примерами Delphi (на рис. 6.17 изображен мо- мент воспроизведения именно этого файла). Учтите, что компо- нент Animate может воспроизводить далеко не все файлы .avi. В атом отношении гораздо большими возможностями обладает компонент MediaPlayer, который будет рассмотрен в следующем разделе 6.2.4. A Animate имеет смысл использовать в основном для воспроизведения клипов Windows.
Грофмка и 1иу»тимедис 6.2.4 Универсальный проигрыватель MedlaPlayer Б Delphi (начиная с Delphi 2) имеется компонент MediaPlay- ег — универсальный проигрыватель аудио- и видео-информации. Этот медиа-плеер расположен на странице System библиотеки ком- понентов. Он инкапсулирует интерфейс управления носителями (Media Control Interface — MCI) Windows 95/98 и Windows NT. Компонент можно использовать в двух режимах. Во-первых, можно предоставить пользователю возможность управлять вос- произведением информации с помощью кнопочного интерфейса, напоминающего панель управления различными проигрывате- лями. Во-вторых, можно сделать сам компонент невидимым и управлять воспроизведением информации с помощью его методов. Пользовательский интерфейс медиа-плеера представлен на рис. 6.18. Он имеет ряд кнопок, управляемых мышью или клави- шей пробела и клавишами со стрелками. Рис. 6.1В Понель компонента MedioPloyer Назначение кнопок (перечисляются слева направо): Кнопка Play _ Ранее Действие Воспроизведение Пауза воспроизведения или записи. Если медиа-плеер в момент щелчка уже в состоянии паузы, то воспроизведе- ние или запись возобновляются Stop Останов воспроизведения или записи Next Переход иа следующий трек или на конец ,Prev Step Переход на предыдущий трек или на начало Перемещенве вперед на заданное число кадров Back Перемещение назад на заданное число кадров Record Начало записи Eject Освобождение объекта, загруженного в устройство
Каждой кнопке медиа-плеера соответствует метод, осуществ- ляющий по умолчанию требуемую операцию: Play, Pause, Stop, Next, Previous, Step, Back, StartRecording, Eject. Тип устройства мультимедиа, с которым работает медиа-пле- ер, определяется его свойством De viceType. Если устройство мультимедиа хранит объект воспроизведения в файле, то имя файле задается свойством FileName. По умолчанию свойство De- viceType имеет значение dtAutoSelect. Это означает, что ме- диа-плеер пытается определить тип устройства, исходя из рас- ширения имени файла FileName- Егце одно свойство MediaPlayer — AutoOpen. Если оно уста- новлено в true, то медиа-плеер пытается открыть устройство, указанное свойством DeviceType, автоматически во время своего создания в процессе выполнения приложения. Воспроизведение видео информации по умолчанию произво- дится в окно, которое создает само открытое устройство мульти- медиа. Однако это можно изменить, если в свойстве Display ука- зать оконный элемент, в котором должно отображаться изобра- жение. Это может быть, например, форма или панель. Можно также задать в свойстве DisplayRect типа TRect (свойство только времени выполнения) прямоугольную область этого окна, в кото- рую должно выводиться изображение. Для задания свойства Dis- playRect можно использовать функцию Rect. Однако, в данном свойстве использование этого типа на совсем обычно. Первые две координаты, как и обычно, задают положение левого верхнего угла изображения. А даа следующих числа задают ширину и вы- соту изображения, а не координаты правого нижнего угла. На- пример, оператор MediaPlayerl.DisplayRect:=Rect(10,10,200, 200); задает для вывода область с координатами левого верхнего угна (10, 10), длиной и шириной, равными 200. В компоненте MediaPlayer определены события OnClick и Оп- Notify. Первое из них происходит при выборе пользователем од- ной из кнопок медиа-плеера и определено как type TMPBtnType=(btPlay,btPause,btStcp,btNext,btPrev, btStep,btBack,btRecord, btEject); procedure(Sender:TObject; Button;TMPBtnType; var DoDefault: Boolean); Параметр Button указывает выбранную кнопку. Параметр DoDefault, передаваемый как var, определяет выполнение (при значении true по умолчанию) или отказ от выполнения стандарт- ного метода, соответствующего выбранной кнопке.
Графике и мультимедиа 207 Событие OnNotify происходит после возвращения очередного метода, если свойство медиа-плеера Notify было установлено в true. Способ возврата любого метода медиа-плеера определяется свойством Wait. Если установить Wait равным false, то возвра- щение управления в приложение происходит сразу после вызова метода, не дожидаясь завершения его выполнения. Таким обра- зом, задав Notify равным true и Wait равным false, можно обес- печить немедленный возврат в приложение и отображения поль- зователю текущего состояния объекта мультимедиа. Свойства Notify и Wait действуют только на один очередной метод. Поэтому их значения надо каждый раз восстанавливать в обработчиках событий OnClick или OnNotify. В обработчиках событий можно читать свойство Mode, харак- теризующее текущее состояние устройства мультимедиа. Можно также читать и устанавливать ряд свойств, характеризующих размер воспроизводимого файла и текущую позицию в нем. Бот, собственно, в конспективном виде основная информация о компоненте MediaPlayer. А теперь попробуйте все это на прак- тике. Простое и в то же время мощное приложение можно сде- нать очень легко. Начните новый проект и перенесите на форму компоненты MediaPlayer, MainMenu, Label и OpenDialog. В фи- льтре компонента OpenDialog можно, например, задать: ауди *.wav, *.mid *. wav;* .raid видео *.avi *.avi все файлы *.* В меню достаточно задать одну команду: Фойл [ Открыть. Обра- ботчик события OnClick этой команды может содержать опера- тор if OpenDialogl.Execute then with MediaPlayerl do begin Labell.Caption:=’Файл: 1+ OpenDialogl.FileName; FileName:=OpenDialogl.FileName; Open; end; который открывает устройство мультимедиа, соответствующее выбранному пользователем файлу. При этом надо проследить, чтобы в компоненте MediaPlayer свойство DeviceType равнялось dtAutoSelect- Это обеспечит автоматический выбор соответству- ющего устройства мультимедиа исходя из расширения выбран ного файла.
208 Глава 6 В компоненте MediaPIayer при желании можно указать имя файла FileName, открываемого в момент качала выполнения приложения. Тогда надо установить свойство AutoOpen в true. Впрочем, это, конечно, не обязательно. Вот и все. Можете выполнять свое приложение и наслаждать- ся музыкой или фильмами (см. рис. 6.19), если, конечно, все во- просы, связанные с настройкой мультимедиа на вашем компью- тере решены. Универсольный про- игрыватель с про- смотром видео фай- лов в автоматически создаваемом окне Ч’яйя: z\vJ*:BSUiiiM=nGa(o\Spbcocp*.evi' В приложении рис. 6.19 просмотр видео файлов осуществля- ется в окне, автоматически создаваемом компонентом MediaP- Iayer. Если хотите отображать окно в каком-то указанном вами оконном компоненте вашего приложения, то это можно сделать, например, следующим образом. Поместите на форму компонент Panel, сотрите текст в ее свойстве Caption и растяните панель до желательных размеров. Затем в свойстве Display компонента Ме- рис. 6.20 Универсальный проигрыватель с просмотром видео файлов в указанном окне
Гроф*«о и мультимедиа 209 diaPIayer выберите из выподающего списка имя панели Panell. И это все. Запустите приложение (см.рис. 6.20) и можете про- сматривать кинофильмы в указанном вами окне. Чтобы все-таки использовать какие-то события компонента MediaPlayer, давайте немного усложним приложение. Дабавим в него еще две метки (рис. 6.21), в которых будем отображать со- стояние открытого устройства мультимедиа и последнюю вы- званную операцию. Рис. 6.21 Приложение универсального проигрывате- ля с отображением состояния Код, обеспечивающий подобную обратную связь в приложе- нии, может быть следующим. const ModeStr: array[TMPModes] of string = ('He готово', 'Остановлено', 'Воспроизведение', ’Запись’, ’Поиск', 'Пауза’, 'Открыто'): ButtonStr: array[TMPBtnType] of string = {’Воспроизведение *, ’Пауза', 'Стоп', 'Следующий', 'Предыдущий', 'Вперед‘'Назад’, 'Запись 1, 'Конец'); procedure TForml.OpenClick(Sender: TObject), begin if OpenDialogl.Execute then with MediaPlayerl do begin FileName:=OpenDialcgl.FileName; Labell.Capticn:=*®aiin: '+FileName; Open; Notify:=true; end; procedure TForml.MediaPlayerlNotifу(Sender: TObject); begin with MediaPlayerl do begin Label?.Caption ;='Состояние: ’+ModeStr[Mode]; {Переустановка Notify, чтобы событие произошло в следующий раз) Notify := True; end;
210 procedure TForml.FormCreate(Sender: TObject); begin with MediaPlayerl do begin Labell.Caption:=’Файл; '+FileName; Label?.Caption ;-=1 Состояние: * +ModeStr [Mode]; Notify :=>t rue; procedure TForml.MediaPIayer1C1iek{Sender: TObj ect; Button; TMPBtnType; var DoDefault: Boolean); begin Label3.Caption:=* Операция: •+ButtonStr[Button]; [Переустановка Notify, чтобы произошло событие OnNotify} MediaPlayerl.Notify := True; end; Запустите приложение и проверьте его в работе.
Глава 7 Развертывание приложений 7.1 Интернационализация приложений Если вы создали прекрасное приложение, которое предназна чаете для международного рынка или хотя бы для международ- ной демонстрации, то вам надо позаботиться о его интернациона- лизации. Надо, чтобы в зависимости от того, какой язык уста- новлен в Windows на конкретном компьютере, ваше приложение автоматически разговаривало бы ня этом языке. Начиная с Delphi 5 подобная интернационализация приложе- ний существенно облегчилась. Правда, для этого надо спроекти- ровать приложение так, чтобы оно поддавалось интернационали- зации- Для этого следует изолировать все ресурсы, которые дол- жны изменяться в процессе локализации. То есть перенести в файлы .dfm и .res все, что может изменяться при смене языка. Например, все тексты, используемые в приложении, надо пере- нести в ресурсы с помощью ключевого слова resonreestring. Все далее изложенное вы можете применить к любому из име- ющихся у вас приложению. Но лучше давайте разработаем со- всем простенькое приложение, чтобы на его примере рассмотреть методику интернационализации. Откройте новое приложение, задайте какой-нибудь русский текст в заголовке формы (свойство Caption), например, «Интернационализация». Поместите на форму метку и разместите в ее надписи (Caption) текст, напри- мер, «Мое приложение*. Эти тексты нужны нам, чтобы видеть, как они будут меняться при изменении языка. Поместите в раз- дел implementation модуля КОД resoii rcestring sTitle = ‘Подтвердите’; sText = ’Действительно котите закрыть приложение?’; а в обработчик события OnCloseQuery формы поместите оператор CanClose:=Application.MessageBox(PChar(sText), PChar(STitle) ,MB_YESNOCANCEL+MB_ICONQUESTIONJ=IDYES; Этот оператор (см. раздел 5.1 главы 5) при закрывании прило- жения предлагает пользователю диалоговое окно с просьбой под- твердить окончание работы (рис. 7.1 в). Если пользователь на- жмет кнопку Да, то функция MessageBox вернет значение IDYES, параметр CanClose обработчика станет равен true и при- ложение закроется. При любом другом ответе пользователя фун-
Глиио 7 кция MessageBox вернет ие IDYES, параметр CanClose будет ра- вен false, что означает отказ от закрытия приложения. Рис. 7.1 Приложение при установленном русском (а) и английском (б) языке (при русифицированной версии Windows) В приведенном коде обработчика надписи, передаваемые в диалоговое окно запроса, ие помещаются непосредственно в текст оператора- Вместо этого используются константы, опреде- ленные ключевым словом resourcestring. Эти константы зано- сятся в ресурсы приложения. Именно это является условием воз- можности интернационализации приложения. Сохраните созданное приложение, запустите его на выполне- ние и убедитесь, что при попытках любым способом закрыть при- ложение оно сопротивляется, предлагая вам подтвердить свое желание окончить работу (рис. 7.1 а). Теперь давайте займемся интернационализацией нашего при- ложения. Проще всего это сделать, выполнив команду File | New и выбрав на странице New окна Депозитария пиктограмму Resource DLL wizard — Мастер DLL ресурсов. Предварительно приложение должно быть сохранено на диске и откомпилировано (у нас уже это сделано). Мастер покажет вам серию экранов, которые, вероятно, ие имеет смысла описывать, так как в них в большинстве случаев вы можете ничего не делать, просто нажимая кнопку Next — сле- дующее окно. Исключение составляет окно, представленное на
Fnanefwoime приложении рис. 7.2. В нем в списке вы должны установить индикаторы тех языков, для которых хотите создать варианты своего приложе- ния. Языков очень много, так что при желании вы можете сде- лать свое приложение настоящим полиглотом и распространять его по всему миру. Впрочем, для начала выберите какой-то один язык, например, английский. Впоследствие вы сможете добав- лять к своему приложению другие языки. Русский язык тоже можете указать, хотя это вовсе не обязательно. Он все равно ва- ляется базовым, т.е. исходным. Впрочем, для большей симмет- рии дальнейшей работы задайте и его. Рис. 7.2 Выбор языков при- ложения Пройдя через серию окон и нажав в последнем из них кнопку Finish, вы, вероятнее всего, увидите окно, показанное на рис. 7.3. В нем содержится сообщение, что ие обнаружен файл .drc — тек- стовый файл ресурсов. Вам предлагается автоматически пере- компилировать проект с опциями, обеспечивающими создание этого файла. Следует согласиться с этим и нажать кнопку Да. Теперь все сделано, чтобы ваш проект смог заговорить на раз- ных языках. Перед вами автоматически откроется окно Менед- жера Трансляции (рис. 7.4), в котором вы можете осуществить трансляцию ресурсов на разные языки. В левой панели окна вы видите дерево ресурсов форм (Forms) и ресурсов проекта (Resource Script) для разных языков. Выделите, например, в английском языке вершину модуля формы Unitl. Вы увидите в правой панели (см. рис. 7.4) свойства формы и ее компонентов, допускающие трансляцию. В первом столбце Id (на рис. 7.4 он показан ие пол- ностью) расположены идентификаторы свойств. Во втором столб- це приводятся значения этих свойств в исходном русском вари- анте, следующий столбец указывает, транслировалось или нет данное свойство, а в следующем столбце вы видите значение свойства в английском варианте. Это значение вы можете редак-
Сообщение Мастера DLL ресурсов Охно Менеджера Трансляции тировать. Редакцию можно осуществлять или непосредственно в ячейке таблицы, или можно, выделив ячейку, нажать крайнюю правую быструю кнопку (см. рис. 7.4) и вызвать специальный многострочный редактор. Вы можете в транслируемом варианте изменить шрифт, мно- жество символов и т.п., чтобы сделать тексты доступными на со- ответствующем языке. Но главное, что надо сделать — перевести тексты надписей на форме и ее компонентах. Чтобы это было проще осуществить, можно посоветовать предварительно щелк- нуть на заголовке столбца Русский. Тогда строки таблицы станут упорядочены в алфавитном порядке значений свойств русского варианта. А поскольку русские символы считаются распложен- ными после латинских, то все надписи на русском языке соберут- ся вместе — внизу таблицы, как показано на рис. 7.4. В нашем примере надо перевести на русский язык надписи «Интернацио- нализация» — «Internationalization» и «Мое приложение» — «Му Application». Аналогичным образом надо заменить строки ресурсов в вер- шине английского варианта Project 1DRC, содержащие русские надписи: «Подтвердите» можно заменить на «Confirm*, а «Дей- ствительно хотите закрыть приложение?» на «Do you want to quit the Application?».. При переходе к вершине Projecfl_DRC вам будет задан вопрос, хотите ли вы сохранить иеменения в вершине Unitl. Аналогич- ный вопрос о сохранении изменении будет вам задан при выходе
из окна Менеджера Трансляции. Конечно на эти вопросы следует ответить положительно. По окончании всего этого вы увидите, что создалась группа проектов. С ней проще всего работать, вызвав окно Менеджера проектов (команда View | Project Monoger), показанное для нашего примера на рис. 7.5. Оно содержит вершину Projectl.exe, отобра- жающую сам проект, и вершины ProjectТ.ели и Pro|edl.rus, отобра- жающие соответственно ресурсы английского и русского вариан- тов. Выбрав вершину Form! того или иного варианта, вы можете увидеть, как выглядит ваша форма в русском или английском исполнении. Вершины Projectl_DRC.rc покажут вам файлы ресур- сов обоих вариантов. Но не исправляйте их вручную — это не приведет ни к чему хорошему. Рис. 7.5 . Окно Менеджера Проектов группы оттранслированных вари- антов приложения Не исправляйте вручную в интернационализированном проекте файлы ре- сурсов. Все исправления делайте только через окно Менеджера Трансляции. Сохраните файл группы проектов и попробуйте с ним рабо- тать- Активизируйте вершину Project 1 .ехе. Для этого можете сде- лать на ней двойной щелчок, или выделить ее и нажать кнопку Activate, или нажать в инструментальной полосе среды Delphi ма- ленькую кнопочку рядом с быстрой кнопкой Run. После этого вы- полните ваше приложение. Вы увидите ту же картину, что и ра- ньше (рис. 7.1 а). А теперь выполните команду Project | Languages. Всплывет кас- кадное меню с разделами Add (добавить новый язык). Remove (удалить один из языков), Set Active (сделать активным один из языков) и Update Resource DLLs (обновить DLL ресурсов).
216 Глава 7 ИНВВВНЙШНВЦ I Все разделы команды Project | Languoges, кроме команды Add, доступны, только если у вас в окне Менеджера Проектов активизирована вершина головного файла проекта (в нашем примере — Projectl.exe) или корневая 1 вершина группы. Команд Add приведет вас в одно из окон Мастера DLL ресур- сов, в котором вы можете указать проект и далее добавить к нему новый язык. Команда Remove приведет вас в окно, в котором вы увидите список введенных в проект языков и около каждого язы- ка будут стоять индикаторы. Их вы можете включить у тех язы- ков, которые хотите удалить. Впрочем, удаление коснется толь- ко возможности делать соответствующий язык активным. Сами DLL ресурсов на диске сохранятся. Команда Update Resource DLLs обновляет DLL ресурсов. Выпол- няйте ее после любых изменений и вообще почаще работайте с ней в случаях возникновения каких-то недоразумений. Команда Set Active делает активным один из языков. В ответ на эту команду появляется диалоговое окно, в котором вы можете выбрать одни из языков для отладки вашего приложения, или выбрать <поле>. Последнее означает, что выбор варианта осуще- ствляется автоматически в зависимости от языка, установленно- го в Windows. Установите активным русский язык (если вы ие указывали его как один из языков при работе в окне рис. 7.2, то его не будет в списке команды Set Active). Выполнив проект, вы увидите, что ничего в его работе, конечно, не изменилось. Если же вы сделаете активным английский язык и после этого выпол- ните проект, то увидите (рис. 7.1 б), что ваше приложение заго- ворило на английском изыке. Правда, вас могут смутить остав- шиеся русскими надписи на кнопках вызываемого диалогового окна. Эти надписи определяются установленной у вас версией Windows. Если вы выполните приложение на компьютере с анг- лоязычной версией Windows, то надписи на кнопках будут анг- лийскими. То, что вы сейчас делали — это только активизация того или иного языка при отладке. К действительному поведению прило- жения при выполнении его ие из среды Delphi установки языка отношения ие имеют. Можете проверить это. Активизируйте командой Set Active раздел <попе>. Сохраните ваш проект. За- кройте Delphi. Выполните ваш файл (Projectl.exe) средствами Windows, например, программой «Проводник». Приложение бу- дет разговаривать по-русски. А теперь установите в Windows анг-
Разввргывш лийский язык. Для этого выполните программу «Панель управ- ления», щелкните в ее окне на пиктограмме Язык и стандарты и на странице Региональные стандарты выберите из выпадающего спис- ка английский язык. Щелкните на ОК. Вам будет предложено перезагрузить компьютер, чтобы ваша установка вступила в силу. Согнаситесь на перезагрузку, а после нее опять выполните вне Delphi ваше приложение. Вы увидите, что оно разговаривает по-английски. А ведь вы ничего с ним не делали! Оно само поня- ло установленный в системе язык и заговорило на нем. Вот это и есть интернационализация приложения. Порадуйтесь на свое создание, которое стало полиглотом, но не забудьте опять вер- нуться к нормальным установкам, указав с помощью «Панели управления», что вы все-таки хотите работать с русским язы- ком. Не забудьте, что если вы хотите распространять свое интернационализиро- ванное приложение, то вместе с выполняемым модулем надо распростра- нять файлы соответствующих языков (в нашем примере Projectl.enu). Мы рассмотрели вариант интернационализации приложения с помощью Мастера DLL ресурсов. Но в дальнейшем вы можете работать с приложением: добавлять, например, новые языки, удалять введенные, изменять перевод и т.п., ие прибегая к помо- щи Мастера. Для этого имеется команда View | Tronslotiori Monager, вызывающая окно Менеджера Трансляции (рис. 7.4). В нем вы можете ие только редактировать свой перевод, но и добавлять но- вый язык (быстрая кнопка со знаком «+») или удалять имею- щийся (быстрая кнопка со знаком «-»). В обоих случаях произой- дет автоматическое обращение к тем или иным окнам Мастера DLL ресурсов. Только имейте в виду, что команда View | Translation Manager доступна, только если у вас в окне Менеджера Проектов активизирована вершина головного файла проекта или корневая вершина группы. 7.2 Автономные приложения и пакеты Когда вы завершили в общих чертах разработку своего проек- та, надо подумать, как вы будете передавать его пользователям. Одним из основных достоинств Delphi, начиная с Delphi 1, яв- лялось то, что в результате проектирования создавался автоном-
218 Глоио 7 ный выполняемый модуль .ехе, т.е. приложение и все его ресур- сы размещались в одном выполняемом файле. Только этот файл и требовалось передавать пользователям. Однако, если у вас со- здано много выполняемых файлов или если вам надо передавать их по сети множеству пользователей, то размеры файлов стано- вятся существенным критерием разработки. Стремление умень- шить затраты на хранение и распространение выполняемых фай- лов привело фирму Borland к концепции пакетов, появившихся впервые в Delphi 3. Пакеты (packages) — это специальные динамически присоеди- няемые библиотеки, содержащие библиотеки визуальных компо- нентов и другие объекты, функции, процедуры и т.д. Эти DLL по- зволяют вам создавать очень небольшие выполняемые модули, обращающиеся за поддержкой к пакетам. Вы можете также скомпилировать в пакеты свои собственные компоненты и биб- лиотеки. Файлы пакетов имеют расширение .dpi. Пакеты разделяются на два типа: пакеты времени проекты рования и пакеты времени выполнения. Пакеты времени проектирования Delphi вызывает в процессе проектирования. Они используются только самой Delphi. Так что на этих пакетах мы останавливаться ие будем. Пакеты времени выполнения содержат библиотеки визуаль- ных компонентов Delphi. Вы можете добавить к Delphi заказные пакеты, разработанные вами или где-то приобретенные. При ис- пользовании пакетов времени выполнения вы должны переда- вать пользователю не только ваш выполняемый модуль, но и все пакеты времени выполнения, которые используются им. За счет того, что большая часть кодов помещается в этих пакетах, разме- ры ваших выполняемых модулей существенно сокращаются. На- пример, модуль в 300 Кб может быть сокращен примерно до 15 Кб. Вы передаете пользователям один раз все пакеты и уста- навливаете их на компьютерах клиентов. А затем, когда вы дела- ете новые приложения, вам достаточно передавать только небо- льшие исполняемые модули. Чтобы использовать пакеты в вашем приложении, вы должны сначала обратиться к поддержке пакетов, а затем компилировать и строить свое приложение. Давайте рассмотрим простой пример построения приложения с использованием пакетов и без них, чтобы сравнить получающиеся результаты. Построим простейшее приложение Delphi — с пустой формой. 1. Откройте новый проект. Сохраните его под именами по умол- чанию Projectl.dpr и Unitl.pas.
Родаертыасниа приложений 2. Выполните команду Project | Build. В результате будет создан выполняемый модуль projectl .ехе без поддержки пакетов. 3. Посмотрите размер получившегося файла, воспользовавшись для этого, например, программой Windows «Проводник» или выполните команду Project | Informotion for project. Команда от- крывает окно информации о проекте, которое предстааленно на рис. 7.6 а. Окно информации о при- ложении без поддержки пакетов времени выполне- ния (о) и с поддержкой (б) В окне вы можете видеть число строк откомпилированного кода (Source Compiled), размер, занимаемый в памяти выполняе- мым кодом без учета отладочной информации (Code Size), объем оперативной памяти, необходимой для хранения глобальных пе- ременных (Dola Size)» объем оперативной памяти, необходимой для хранения локальных переменных (Initial Stack Size), размер ре- зультирующего файла (File size). Строка Status Information сооб- щает об успешности (или неудаче) при компиляции приложения.
220 Панель Package Used показывает список пакетов времени выпол- нения, если приложение использует их поддержку. На рис. 7.6 а вы можете видеть, что размер вашего файла со- ставляет 288768 байтов или 282 Кб (от версии к версии Delphi этот размер нарастает; в Delphi 4, например, он составляет 275 Кб, в Delphi 3 — менее 200 Кб). Продолжим наш эксперимент. 4, Выполните команду Project | Options, если вы работаете в Delphi 4 или 5. В Delphi 3 вам надо выполнить команду View | Project Manager и в появившемся диалоговом окне щелкнуть на кноп- ке Options. 5. В раскрывшемся окне опций проекта Project Options выберите страницу Packages — пакеты. Ее вид в Delphi 5 показан на рис. 7.7. 6. Отметьте на странице индикатор Built with runtime packages (строить с поддержкой пакетов времени выполнения) и щелк- ните на ОК. 7. Перестройте ваш выполняемый модуль, выполнив команду Project | Build (как в пункте 2). 8. Посмотрите теперь размер вашего файла projectl.exe с под- держкой пакетов (рис. 7.6 б). Размер сократился до 13824 бай- тов или до 14 Кб (этот размер, как и размер без поддержки па- кетов, тоже нарастает с каждой новой версией Delphi). Рис. 7.7 Страница Packages окна опций проекта
Рд>»артывание приложений 221 Как видите, выигрыш весьма значительный: без поддержки пакетов размер файла составляет 288768 байтов (282 Кб), а с под- держкой — 13824 байта (14 Кб), т.е. примерно в 20 раз меньше. Но, как видно из рис. 7.6 б, для поддержки вашего приложения требуется пакет VCL50.bpl. Так что если вы надумали использо- вать поддержку пакетов времени выполнения, то вместе со своим приложением вы должны поставлять пользователю откомпили- рованный файл этого пакета — VCLSO.dcp. Размер этого файла составляет 3701 Кб. Это, конечно, очень внушительный размер. Но дело в. том, что этот файл взм надо поставить пользователю один раз и он обеспечит поддержку подавляющего большинства ваших приложений, файлы которых будут небольшими. Давайте прикинем, когда это становится выгодным. Предпо- ложим, что средний размер файла приложения с поддержкой па- кетов времени выполнения — 20 Кб, а без поддержки в 20 раз бо- льше — 400 Кб- Тогда, если пользователь использует в своей ра- боте N приложений, то без поддержки пакетов ему потребуется для хранения файлов N*400 Кб дискового пространства, а при поддержке пакета VCL50 — (3701 + N*20) Кб. Нетрудно посчи- тать, что уже при N = 10 поддержка пакетов становится выгод- ной. Использовать, или не использовать поддержку пакетов време- ни выполнения — вопрос, возникающий только при заключите- льной компиляции уже готового приложения. Сзм процесс про- ектирования никак не изменяется от того, будете или не будете вы использовать поддержку пакетов. А поскольку поддержка па- кетов позволяет существенно уменьшить затраты дискового про- странства под хранение выполняемых модулей, то можно дать следующий совет. Для экономии место но диске при параллельной разработке нескольких проектов или нескольких вариантов одного проекта имеет смысл в процес- се разработки включить поддержку пакетов времени выполнения. Это по- | зеолит вам более чем на порядок сократить размеры генерируемых выпол- няемых модулей. Для использования этой возможности выполните команду Pro- ject | Options, если вы работаете в Delphi 4 или 5. В Delphi 3 вам надо выполнить команду View | Project Manager и в появившемся диалоговом окне щелкнуть на кнопке Options. В раскрывшемся окне опций проекта выберите страницу Packages и на странице Packages включите индикатор Built with runtime packages. Одновре-
322 менно полезно включить в том же окне (рис. 7-7) индикатор Defa- ult, что обеспечит статус этой установки как установки по умол- чанию для всех ваших будущих проектов. Только не забудьте при заключительной компиляции закон- ченного проекта выключить опцию поддержки пакетов, если вы измерены передавать пользователям автономный выполняемый модуль. При распространении приложений с поддержкой пакетов надо иметь в виду, что пакеты используют API Windows, содер- жащийся в различных DLL. Если какая-то из этих DLL у потре- бителя вашего программного продукта ошибочна или не соответ- ствует по дате (версии), у вас могут возникнуть проблемы. Их можно избежать, если проверять свое приложение на той систе- ме, для которой оно предназначено, или на чистых установках Windows 95/9В и NT; тогда сможете быть уверенными, что оно будет выполняться без ошибок, и будете знать, что требуется ва- шему приложению для нормальной работы. В результате вы смо- жете убедиться, что включаете в поставку все необходимые фай- лы, или можете потребовать от пользователя работать на опреде- ленной версии операционной системы с определенными установ- ками путей и т.д. 7.3 Установка и настройка приложения: работа с системным реестром Обсудим коротко вопросы установки вашего приложения на компьютере пользователя. Если это очень простое приложение, то никаких проблем нет — достаточно скопировать выполняе- мый файл приложения с загрузочной дискеты или диска CD НОМ в выделенный для приложения каталог. Если в дальней- шем пользователь решит удалить вашу программу со своего компьютера, ему будет достаточно удалить этот файл. В более сложных приложениях обычно фигурируют различ- ные файлы настройки, конфигурации и т.д., расположенные к тому же в разных катзлогах. В этих случаях установить програм- му на компьютере и удзлить ее, если она стала не нужна, уже го- раздо сложнее. Раньше, в Windows 3.x информация о конфигурации и на- стройках приложения хранились в файлах .ini. Но для того что- бы упорядочить процессы установки и удаления программ, начи- ная с Windows 95 и NT Microsoft требует, чтобы вся информация о конфигурации системы хранилась в системном реестре..Реестр (Registry) — это база данных для хранения информации о сис-
Разаертывоние приложений 223 темной конфигурации аппаратуры, о Windows и о приложениях Windows. Почти все, что в Windows 3.x находилось в файлах .ini, перенесено в Windows 95 и NT в реестр. Реестр имеет иерархиче- скую организацию, которая, содержит много уровней ключей, субключей и параметров. Информация хранится в виде иерархи- ческого дерева, каждый узел которого называется ключом. Ключ может содержать субключи и значения параметров. Реестр делит все свои данные на две категории: характеризую- щие компьютер и характеризующие пользователя. Характери- стики компьютера включают в себя все, связанное с технически- ми средствами, а также с установленными приложениями и их конфигурацией. Характеристики пользователя включают в себя установки по умолчанию для экрана, пользовательские конфигу- рации, информацию о выбранных пользователем принтерах, установки сети. Все субключи относятся к шести основным ключам реестра- Четыре из них определяют характеристики компьютера: Hkey_Locai_Maehine Hkey_Curren t_Conf ig Ilk еуВуП—Data Hkey_Classes_Root Информация о компьютере, включая конфигу- рацию установленной аппаратуры и програм- много обеспечения Информация о текущем оборудовании Динамические данные о состоянии, используе- мые процедурами plug-and-play Информация об OLE, Drag&Drop, клавишах быстрого доступа и пользовательском интер- фейсе Windows 95 Два ключа верхнего уровня определяют характеристики поль- зователя: HkeyUsers Информация о пользователях, включая уста- новки экрана и приложений Hkey_Current_User Информация о пользователе, зарегистрирован- ном в данный момент Реестр хранится в файле SYSTEM.DAT в каталоге Windows. Просмотр и редактирование реестра из Windows осуществляется редактором реестра Regedit.exe (рис. 7-8). Только прежде, чем вы будете вручную что-то редактировать в реестре или опробо-
224 Гло.о 7 Рис. 7.8 Окно редактора реестра Rege- dit.exe вать изложенные ниже приемы работы с реестром ИЗ приложе- ний Delphi, прислушайтесь к следующему совету. Прежде, нем изменять что-то в реестре, сохраните копию его фойла SYSTEM.DAT расположенного в каталоге Windows, в каком-то другом ка- талоге Это позволит вам в случае неудочного вмешательства в реестр вос- становить прежнее состояние этого файла В противном случае ваши экспе- рименты могут закончится плачевно вплоть до необходимости переустоное- ливатъ Windows, Для работы с реестром в Delphi имеется класс TKegistry, опи- санный в модуле registry. Если вы создаете в приложении объект класса TKegistry для работы с реестром, не забудьте обеспечить связь с этим модулем оператором uses registry; Все ключи в объекте класса TKegistry создаются как субклю- чи определенного корневого ключа, записанного в свойстве Root- Key. По умолчанию RootKey = HKEYCURRENTUSER. В каж- дый момент объект типа TRegistry имеет доступ только к одному текущему ключу в иерархии, начинающейся с ключа RootKey. Текущий ключ определяется свойством только для чтения Спг- rentKey. Но это значение вам ничего не скажет — это просто не- которое целое значение. А вот свойство CurrentPath (тоже толь- ко для чтения) содержит строку, включающую имя текущего ключа и путь к нему по дереву. Изменить текущий ключ можно методом ОрепКеу: OpenKey(const Key: String; CanCreate: Boolean): Boolean
Ршвертывоние приложений ИЗ Этот метод открывает ключ Key, делая его текущим для объ- екта. Параметр Key — строка полного пути по дереву ключей к открываемому ключу. Если Key — прустая строка, то текущим делается корневой ключ, указанный свойством RootKey. Пара- метр CanCreate указывает, должен ли создаваться ключ Key, если его нет в реестре. Ключ открывается или создается с досту- пом KJEY~ALL_ACCESS- Создаваемый ключ сохраняется в ре- естре при последующих запусках системы. Запись значений параметров в ключ осуществляется группой методов: Writeinteger, WriteFloat, WriteBool, WriteString и др. Объявления всех этих методов очень похожи. Например: procedure Writeinteger(const Name: string; Value.- Integer); procedure WriteString(const Name, Value: String); Все они заносят значение Value в параметр с именем Name. Имеются аналогичные методы чтения: Readinteger. ReadFloat. ReadBool, ReadString и др. Например: function Readinteger(const Name: String): Integer; function Readstring(const Name: String): String; Посмотрим на примере, как все это можно использовать для установки программы, запоминания ее настроек и для удаления программы. Сделайте простое тестовое приложение. Перенесите на форму три кнопки Button и диалог FontDialog. Первая кнопка (назови- те ее Blast и задайте надпись Install) будет имитировать установ- ку программы. Точнее, не саму установку, поскольку копировать файлы с установочной дискеты мы не будем, а только регистра- цию нашего приложения в реестре. Вторая кнопка (назовите ее BUnlnst и задайте надпись Uninstall) будет имитировать удале- ние программы. Тут мы не будем удалять саму программу с дис- ка (жалко ее!), а только удалим из реестра ссылку на нее. А тре- тья кнопка (назовите ее BFont и задайте надпись Font) будет по- зволять изменять имя шрифта, используемого в форме, и обеспе- чит запоминание этого шрифта в реестре, чтобы в дальнейшем прн запуске приложения можно было читать эту настройку и за- давать ее форме. Ниже приведен текст этого тестового приложения. uses registry; var Reg : TRegistry; procedure TForml.FormCreate(Sender: TObject); begin {Создается объект Reg типа KeyExists) Reg := TRegistry.Create;
Reg.RootKey:=HKEY_LOCAL_MACH1NE; if Reg.KeyExists(’XSoftware\A ProjectsXPl') then begin {Если программа была установлена, то читается настройка шрифта} Reg.ОрепКеу(’\Software\A ProjectsXPl*,true); Font.Name:= Reg.Readstring(’Шрифт') end; procedure TForml.BlnstClick(Sender: TObject); begin (Имитация установки программы) Reg.ОрепКеу('\Software\A Projects',true); Reg.WriteStringCTewa', 'Мои приложения’); Reg.ОрепКеу(’XSoftwateXA ProjectsXPl*,true); Reg.Writestring('Приложение',’TRegistry") ; {Запись в параметр Файл имени и пути к выполняемому файлу) Reg.WriteString('Файл’,ParamStr(0)); (Запись в параметр Шрифт имени шрифта) Reg.WriteString('Шрифт’,Formi- Font.Name); end; procedure TForml.EUnlnstClick(Sender: TObject); begin (Удаление субключа Pl из регистра) Reg.DeleteKey(’XSoftwareXA ProjectsXPl'); end; procedure TForml.FormDestroy(Sender: TObject); begin {Удаление из памяти объекта Reg) Reg.Free; end; procedure TForml.BFontClick(Sender: TObject); begin (Запоминание в реестре имени шрифта) if(FontDialogl.Execute) then begin Font.Assign(FontDialogl.Font); if(Reg.ОрепКеу('\Software\A ProjectsXPl',false)) (Запись имени шрифта только если имеется раздел Р1) then Reg.WriteString('Шрифт1,Formi.Font.Name); end; end; В начале текста следует подключение к приложению модуля registry и объявление переменной Reg типа TRegistry. При со- здании формы приложения в процедуре TFonnl.FormCreate со-
Развертывание приложений 227 здается объект Reg. Его корневым узлом задается ключ HKEY LOCAL MACHINE Затем с помощью метода KeyExists проверяется, существует ли в реестре субключ \Software\A Рго- jects\Pl. Этот ключ, как мы позже увидим, создается при реги- страции нашего приложения в реестре. Так что в настоящем при- ложении, если бы метод KeyExists вернул false, надо было бы выдать какое-то предупреждение о том, что приложение не заре- гистрировано, и завершить работу. Если метод KeyExists вернул true, то далее в процедуре от- крывается методом OpenKey субключ \Software\A Projects\Pl и читается методом Readstring имя шрифта, занесенное в пара- метр Шрифт, в имя шрифта формы. Теперь рассмотрим процедуру TForml.BlnstCIick, имитирую- щую установку программы. В этой процедуре методами OpenKey создается иерархия ключей: A Projects и Pl в субключе Software (этот субключ всегда существует в реестре). В ключ A Projects за- носится один параметр —- Тема со значением Мои приложения. Подразумевается, что этот субключ будет содержать ссылки на все ваши приложения, зарегистрированные в реестре. В ключ Р1 (субключ вашего регистрируемого проекта) заносится три пара- метра. Первый параметр — Приложение со значением TRegistry. Второй параметр — Файл со значением, равным полному имени файла приложения вместе с путем. Это имя получается из нуле- вого параметра командной строки, передаваемого функцией Ра- ramStr(O). В третий параметр — Шрифт заносится имя текущего шрифта, используемого в форме. В настоящей программе уста- новки в эту процедуру надо было бы добавить копирование соот- ветствующих файлов с установочной дискеты. Процедура TForml.BUnInstClick имитирует удаление прило- жения и ссылки на него (субключа Р1) из реестра. В настоящем приложении прежде, чем удалять ключ из реестра, надо было бы прочитать значение параметра Файл и удалить этот файл с дис- ка. Процедура TForml.FormDestroy, срабатывающая при закры- вании формы приложения, удаляет методом Free из памяти вре- менный объект Reg. Процедура TForml.BFontClick вызывает стандартный диалог выбора шрифта, и если пользователь выбрал шрифт, то он при- сваивается форме и его имя заносится в реестр. При этом в мето- де OpenKey второй параметр задан равным false. Это значит, что если ваше приложение не зарегистрировано (субключа Р1 нет в реестре), то запись шрифта не производится.
Сохраните свое тестовое приложение и запустите его на вы- полнение. Нажмите кнопку Install. После этого запустите про- грамму Regedit.exe и посмотрите реестр. Вы увидите там свои ключи и параметры (именно они показаны на рис. 7. В). Щелкни- те на кнопке Uninstall. Вернитесь в окно Regedit.exe и выберите в нем команду Вид | Обновить. Вы увидите, что ключ 1’1 вмемте со всеми своими параметрами исчез из дерева. Опять нажмите кнопку Install, а затем нажмите кнопку Font и выберите шрифт с каким-нибудь другим именем. В окне Regedit.exe вы можете увидеть, что значение параметра Шрифт изменится. Закройте свое приложение и запустите его повторно. Вы увидите, что на форме применен тот шрифт, который вы зарегистрировали в ре- естре. Таким образом, приложение проимитировало установку программы, снятие программы с регистрации и запоминание те- кущих настроек в реестре. Когда вы кончите экспериментировать с этим приложением, удалите с помощью Regedit.exe из реестра ваш ключ A Projects, поскольку приложение удаляло только его субключ Р1. 7.4 Установка и настройка приложения: работа с файлами .IN1 В разделе 7.2 рассказывалось, как регистрировать приложе- ние в системном реестре и фиксировать там текущие настройки приложения. Однако, подобная работа с реестром возможна толь- ко в 32-разрядных Windows 95/98 и NT. Если же вы хотите, что- бы ваше приложение можно было использовать и в Windows 3.x, то вам нздо регистрировать приложение и фиксировать его на- стройки в файлах типа -ini. Для 32-разрядных приложений Mic- rosoft не рекомендует работать с файлами .ini. Впрочем, несмот- ря на это и 32-разрядные приложения, наряду с реестром, часто используют эти файлы. Да и разработки Microsoft не обходятся без этих файлов. Файлы .ini — это текстовые файлы, предназначенные для хра- нения информации о настройках различных приложений. Ин- формация в файле логически группируется в разделы, каждый из которых начинается оператором заголовка, заключенным в квадратные скобки. Например, [Desktop). В строках, следующих за заголовком содержится информация, относящаяся к данному разделу, в форме: <ключ>=<значение> В качестве примера ниже приводится фрагмент - файла ODBC.INI:
[ODBC 32 bit Data Sources) dBASE Files=Microsoft dBase Driver (*.dbf) (32 bit) Excel Files=Microsoft Excel Driver (*.xls) (32 bit) FoxPro Files=Microsoft FoxPro Driver (*.dbf) (32 bit) Text Files=Microsoft Text Driver (*.txr; *.csv) (32 bit) [dBASE Files] Driver32=C:\WINDOWS\S¥STEM\odbcj t32.dll Файлы .mi, как правило, хранятся в каталоге Windows, кото- рый можно найти с помощью функции GetWindowsDirectory. В Delphi работу с файлами .ini проще всего осуществлять с по- мощью создания в приложении объекта типа TlniFile. Этот тип описан в модуле inifiles, который надо подключать к приложе- нию оператором uses (автоматически это не делается). Создается объект типа TlniFile методом Сгеа1е(<имя файла>), в который передается имя файла .ini, с которым он связывается. Файл должен существовать до обращения к методу Create. Для записи значений ключей существует много методов: Wri- teString, Writeinteger, WriteFloat, WriteBool и др. Каждый из них записывает значение соответствующего типа. Объявления всех этих методов очень похожи. Например: procedure WriteString(const Section,Ident, Value: string); procedure Writeinteger(const Section, Ident: string; Value: Longint); Bp всех объявлениях Section — раздел файла, Ident — ключ этого раздела, Value — значение ключа. Если соответствующий раздел или ключ отсутствует в файле, он автоматически создается. Имеются аналогичные методы чтения: ReadString, Readlnts- ger, ReadFIoat, ReadBool и др. Например: function Readstring(const Section,Ident, Default: string): string; function Readinteger(const Section, Ident: string; Default: Longint):Longint Методы возвращают значение ключа Ident раздела Section. Параметр Default определяет значение, возвращаемое в случае, если в файле не указано значение соответствующего ключа. Проверить наличие значения ключа можно методом ValueE- xists, в который передаются имена раздела и ключа. Метод Dele- teKey удаляет из файла значение указанного ключа в указанном разделе. Проверить наличие в файле необходимого раздела мож- но методом SectionExists. Метод EraseSection удаляет из файла указанный раздел вместе со всеми его ключами. Имеется еще ряд методов, которые вы можете посмотреть во встроенной справке Delphi.
азо Посмотрим на примере, как все это можно использовать для установки программы, запоминания ее настроек и для удаления программы. Сделайте простое тестовое приложение, аналогичное рассмот- ренному в разделе 7-3. Перенесите на форму три кнопки Button и диалог FontDialng. Первая кнопка (назовите ее Blnst и задайте надпись Install) будет имитировать установку программы- Точ- нее, не саму установку, поскольку копировать файлы с устано- вочной дискеты мы не будем, а только создание файла -ini в ката- логе Windows. Вторая кнопка (назовите ее BUnlnst и задайте надпись Uninstall) будет имитировать удаление программы. Тут мы не будем удалять саму программу с диска, а только удалим из каталога Windows наш файл -ini. А третья кнопка (назовите ее BFont и задайте надпись Font) будет позволять изменять имя шрифта, используемого в форме, и обеспечит запоминание этого шрифта в файле .ini, чтобы в дальнейшем при запуске приложе- ния можно было читать эту настройку и задавать ее форме. Ниже приведен текст этого тестового приложения, uses inifiles; var InizTIniFile; sFile:string; procedure TForml.FormCreate(Sender: TObject}; var APchar:array[O-.254) of char; begin [Формирование имни файла в каталоге Windows} GetWindowsDirectory[APchar, 255); sFile:“String(APchar) + •\My.ini *; if FileExists(sFile) then begin Ini:=TIniFile.Create(sFile): (Чтение в имя шрифта формы значения ключа Шрифт) Font -Name:=Ini.Readstring{1 Параметры','Шрифт', ’MS Sans Serif’); end; procedure TForml.FormDestroy(Sender: TObject); begin if (Ini - nil) then exit; (Очистка буфера и запись файла на диск) Ini.UpdateFile; [Освобождение памяти) Ini.Free; end; procedure TForml.BlnstClick(Sender: TObject); var F:File;
231 begin (Проверка существования файла .ini} if (not FileExists(sFile)) then begin {Создание файла -ini} AssignFile(F, sFile); Rewrite(f); CloseFile(F) Ini:=TIniFile.Create(sFileJ; end; [Создание раздела Files, ключа main и запись в него имени выполняемого файла вместе с путем} Ini.Writestring(’Files’,'main1, ParamStr(0)); {Создание раздела Параметры, ключа Шрифт и запись в него имени шрифта формы} Ini.WriteString("Параметры*,"Шрифт*,Font.Name); end; procedure TForml.BUnInstClick(Sender: TObject); var FtFile; begin {Удаление с диска файла .ini, если он существует} if FileExists(sFile) then begin AssignFile(F, sFile); Erase(F); end; end; procedure TForml.BFontClick(Sender: TObject}; begin {Выбор шрифта} if (FontDialogl.Execute) then begin {Присваивание выбранного шрифта форме} Font.Assign(FontDialogl.Font); if (Ini <> nil) and Ini-ValueExists{"Параметрм",’Шрифт’) {Запись шрифта в ключ ’’Шрифт” разделе "Параметры"} then Ini.WriteStringCПараметры*.'Шрифт*.Font.Name) ; end; end; В начале текста следует подключение к приложения модуля inifiles и объявление переменной Ini типа TIniFile и переменной sFile, в которой будет формироваться имя файла и путь к нему. При создании формы приложения в процедуре TForml.FormCre- ate формируется имя файла (My.ini) вместе с путем к нему — ка- талогом Windows. Этот путь определяется функцией GetWin- dowsDirectory. Далее функцией FileExists проверяется, сущест- вует ли этот файл, т.е. проведена ли уже установка программы. Если существует, то создается объект Ini, связанный с этим фай- лом и значение ключа Шрифт раздела Параметры читается в имя
232 шрифта формы. Тем самым читается настройка, произведенная при предыдущем выполнении приложения. Теперь рассмотрим процедуру TForml .BlnstClick, имитирую- щую установку программы. В этой процедуре сначала функцией FileExists проверяется, существует ли в каталоге Windows файл My.ini. Если не существует, то последовательным применением функций AssignFile, Rewrite и CIoseFile этот файл (пока пустой) создается. Затем создается связанный с этим файлом объект Ini. Последующие операторы записывают в этот файл два раздела Files и Параметры с соответствующими ключами. В ключ main записы- вается имя приложения с путем к нему. Для этого используется уже применявшаяся в разделе 7.3 функция ParamStr(O). В резу- льтате в созданный файл записывается, например, такой текст: [Files] main=C:\T\Project1.EXE [Параметра] Щрифт=МЗ Sans Serif Процедура TForml.BUnlnstClick имитирует удаление приложе- ния и его файла настройки. В данном случае просто удаляется файл .ini, но в настоящем приложении надо было бы прочитать имя фай- ла (или файлов) приложения из раздела Files и удалить их с диска. Процедура TForml.FormDestroy, срабатывающая при закры- вании формы приложения, сначала методом UpdateFile перепи- сывает содержимое объекта Ini в файл, а затем методом Free уда- ляет из памяти этот временный объект. Процедура TForml.BFontClick вызывает стандартный диалог выбора шрифте, и если пользователь выбрал шрифт, то он при- сваивается форме и его имя заносится в файл -ini. Сохраните свое тестовое приложение и запустите его на выполнение. Нажмите кнопку Install. После этого убедитесь в наличии файла My.ini в каталоге Windows. Можете воспользова- ться для этого программой Windows «Проводник» или любой другой. В частности, можно открыть этот файл просто из среды Delphi. При нажатии кнопки Uninstall файл должен удаляться с диска. Проверьте запись в файл настройки шрифта и чтение ее при последующих запусках. Для этого опять нажмите кнопку Install, а затем нажмите кнопку Font и выберите шрифт с каким-нибудь другим именем. Затем закройте свое приложение и запустите его повторно. Вы увидите, что на форме применен тот шрифт, который вы зарегистрировали в файла настройки. Таким образом, приложение проимитировало установку прог- раммы, удаление программы и запоминание ее текущих настро- ек.
Приложение П.1 Код текстового редактора Ниже приведен полный код текстового редактора. Подготовка его меню и инструментальной панели описано в разделе 1.6.2. Обеспечение подсказок в панели состояния описано в раз- деле 1.9. В тексте приведеи вариант, обеспечивающий подсказки в любой версий Delphi. В разделе 1.9 рассказано, как это можно упростить в Delphi 5. Вид формы приложения приведен на рис. 1.7. Приложение в работе показано на рис. 1.5. unit Unitl; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ComCtrls, ToolWin, StdActns, ActnList, ImgList, Menus, StdCtrls; type TForml = class(TFormJ MainMenul: TMainMenu; MFilel: TMenuItem; MNewl: TMenuItem; MOpenl: TMenuItem; MClosel: TMenuItem; N1: TMenuItem; MSavel: TMenuItem; MSaveAsl: TMenuItem; N2: TMenuItem; MPreviewl: TMenuItem; MPrintl: TMenuItem; MPrintSetupl: TMenuItem; N3: TMenuItem; MExitl: TMenuItem; N4: TMenuItem; MEditl: TMenuItem; MUndol: TMenuItem; N5: TMenuItem; MCutl: TMenuItem; MCopyl: TMenuItem; MPastel: TMenuItem;
234 Приложение 1 N6: TMenuItem; MFindl: TMenuItem ; MReplacel: TMenuItem; MFormatl: TMenuItem; MFontl: TMenuItem; MParagrl: TMenuItem; MWindowl: TMenuItem; MNewWindl: TMenuItem; N7: TMenuItem; MCascadl: TMenuItem; HArrangel; TMenuItem; MHorizontl: TMenuItem; MVe r11: TMenuItem; MArrIcon1: TMenuItem; MHelpl: TMenuItem; MCallHel.pl: TMenuItem; N8: TMenuItem; MAboutl: TMenuItem; Imagel: TlmageList; ActionListl: TActionList; AUndo: TAction; ACut: TEditCut; ACopy: TEditCopy; APaste: TEditPaste; AArrangelcons: TWindowArrange; ACascade: TWindowCascade; ATileHorizontally: TWindowTileHorizontal; ATileVertically: TWindowTileVertical; ACreate: TAction; AOpen: TAction; ASave: TAction; ASaveAs: TAction; APrint: TAction; APrinterSetup: TAction; AExit: TAction; AFind: TAction; AReplace: TAction; AFont: TAction; ToolBarl: TToolBar; TBCreate: TToolButton; TBOpen: TToolButton; TBSave: TToolButton; ToolButton4: TToolButton; TBPrint: TToolButton; TooIButton6: TToolButton; TBUndo: TToolButton; TBCutz TToolButton; TBCopy: TToolButton; TBPaste: TToolButton; ToolButtonll: TToolButton;
Код текстового редактора TBFind: TToolButton; TBReplace: TToolButton; ToolButtonl'): TToolButr.cn; TBFont: TToolButton; OpenDialogl: TOpenDialog; SaveDialogl: TSaveDialog; FontDialogl: TFontDialog; PrinterSetupDialogl: TPrinterSetupDialog; PrintDialogl: TPrintDialog; FindDialogl: TFindDialog; ReplaceDialogl: TReplaceDialog; MAlign: TMenuItem; MLeft: TMenuItem; MRight: TMenuItem; MCenter: TMenuItem; ALeft: TAction; ARight: TAction; ACenter: TAction; ToolButtonl6: TToolButton; TBLeft: TToolButton; TBCenter: TToolButton; TBRigbt: TToolButton; RichEditl: TRichEdit; ABullet: TAction; TBBullet: TToolButton; MBullet: TMenuItem; StatusBarl: TStatusBar; PopupMenu1: TPopupMenu; N9: TMenuItem; N10: TMenuItem; Nil: TMenuItem; N12 : TMenuItem; N13: TMenuItem; N14: TMenuItem; N15: TMenuItem; N16: TMenuItem; N17: TMenuItem; procedure ACreateExecute(Sender: TObject}; procedure AFontExecute(Sender: TObject); procedure AOpenExecute(Sender: TObject}; procedure APrinterSetupExecute(Sender: TObject); procedure APrintExecute(Sender: TObject); procedure ASaveAsExecute(Sender: TObject); procedure ASaveExecute(Sender: TObject}; procedure FontDialoglApply(Sender: TObject; Wnd: HWND); procedure ALeftExecute(Sender: TObject); procedure ACenterExecute(Sender: TObject); procedure ARightExecute(Sender: TObject); procedure ABulletExecute(Sender: TObject);
236 Приложение 1 procedure StatusExecute(Sender: TObject); procedure RichEditlKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); procedure RichEditlMouseDown(Sender: TObject; Button:TMouseButton;Shift:TShiftState;X,Y:Integer); procedure FormCreate(Sender: TObject); procedure DisplayHint(Sender: TObject); procedure ACutExecute{Sender: TObject); procedure APasteExecute(Sender: TObject); procedure AUndoExecute(Sender: TObject); procedure ACopyExecute(Sender: TObject); procedure AFindExecute(Sender: TObject); procedure FindDialcglFind(Sender: TObject); procedure ReplaceDialoglReplace(Sender: TObject); procedure AReplaceExecute(Sender: TObject); procedure ReplaceDialoglFind(Sender: TObject); procedure AExitExecute(Sender: TObject); private { Private declarations ) public { Public declarations ) end; var Forml: TForml; implementation {SR *.DFM) const sFileNatne: string = var SPos:integer; procedu re TForml.StatusExecute(Sender: TObj ect); begin StatusBarl.Panels.Items[0).Text:= IntToStr(RichEditl.CaretPos.y+1) + : ’+IntToStr(RichEditl.CaretPos.x+1); if (RichEditl.Modified) then StatusBarl.Panels-Items[1J -Text:=’модиф.’ else StatusBar 1.Panels. Items [ 1 ]. Text :=” ; case (RichEditl.Paragraph.Alignment) of taLeftJustify: begin ALeft.Checked:=true; MLeft.Checked:=true; TBLeft.Down:=true; end; taCenter: begin ACenter.Checked:=true ; MCenter.Checked:=true;
Код тяксгоюго редактора TBCenter.Down:=true: end; taRightJustify: begin ARighc.Checked:=true; MRight.Checked:=true; TBRight.Down:=true; end; end; end; procedure TForml.DisplayHint(Sender: TObject); begin StatusBarl.Panels.Items[2].Text:=Application.Hint; end; procedure TForml.ACreateExecute(Sender: TObj ect); var res:integer; begin if (RichEditl.Modified) then begin res:=Application.MessageBox( 'Текст в окне был изменен.Сохранить его?1, 'Подтвердите сохранение файла',MB_YESNOCANCEL); case (res) of IDYES: ASaveExecute(Sender); IDCANCEL: Exitr- end; RichEditl.Clear; sFileName;='"; end: procedure TForml.AFon tExecu te(Sender: TObj ect); begin if FontDialogl.Execute then RichEditl.SelAttributes.Assign(FontDialogl.Font); RichEditl.SetFocus; end; procedure TForml.AOpenExecute(Sender: TObject); begin OpenDialogl.FileName := sFileName; if (OpenDialogl.Execute) then begin sFileName := OpenDialogl.FileName; RichEditl.Modified:=false; RichEditl.Lines.LoadFromFile{sFileName); end;
procedure TForml.APrinterSetupExecute(Sender: TObject); begin PrinterSetupDialogl.Execute; end; procedure TForml-APrintExecute(Sender: TObject); var i:integer; begin if(PrintDialogl.Execute) then for i:=l to PrintDialogl.Copies-1 do RichEditl.Print(1Печать PRichEditl'); end; procedure TForml.ASaveAsExecute(Sender: TObject); begin SaveDialogl.FileName := sFileName; if (SaveDialogl-Execute) then begin sFileName := SaveDialogl.FileName; RichEditl.Lines.SaveToFile(SaveDialogl.FileName) ; RichEditl.Modified:=false; end; procedure TForml.ASaveExecute(Sender: TObject); begin if (sFileName = ’•) then ASaveAsExecute(Sender) else RrchEditl.Lines.SaveToFile(sFileName); RichEditl.Modified:=false; end; procedure TForml.FontDialoglApply(Sender: TObject; Wnd: HWND); begin RichEditl.SeiAttributes-Assign(FontDialogl.Font) ; end; procedure TForml.ALeftExecute(Sender: TObject); begin RichEditl.Paragraph.Alignment:=taLeftJustify; ALe f t.Cheeked:=true; end; procedure TForml.ACenterExecute(Sender: TObject); begin RichEditl-Paragraph-Alignment:=taCenter; ACenter.Checked:=true;
Код текстового редактора 239 procedu re TForml.ARightExecute(Sender: TObject); begin RichEdi 11.Paragraph.Alignment:=taRightJusti f y; ARigb t.Checked:=true; end; procedure TForml.ABulletExecute(Sender: TObject); begin if(ABullet.Checked) then RichEditl.Paragraph.Numbering:=nsNone else RichEditl.Pa ragraph.Numbe ring:=nsBullet; ABullet.Checked: =not ABullet.Checked; end; procedure TForml.RichEditlKeyUp(Sender: TObject; rar Key: Word; Shift: TShiftState); begin StatusExecute(Sender); SPos^RichEditl.SelStart; end; procedure TForml.RichEditlMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin StatusExecute(Sender); SPos:=RichEditl.SelStart; end; procedure TForml.FormCreate(Sender: TObject); begin RichEditl.Paragraph.Firstlndent:=0; RichEditl.Paragraph.Leftindent:=- RichEditl.Paragraph.Firstlndent; Application.OnHint:=DisplayHint; RichEdi 11.Modified:=faIse; StatusExecute(Sender); end; procedure TForml.ACutExecute(Sender: TObject); begin RichEditl.CutToClipboard; end; procedure TForml.APasteExecute(Sender: TObject); begin RichEditl.PasteFromClipboard; end;
240 Приложение 1 procedure TForml.AUndoExecute(Sender: TObject); begin RichEditl.Undo; end; procedure TForml.ACopyExecute(Sender: TObject}; begin RichEditl.CopyToClipboard; end; procedure TForml.AFindExecute(Sender: TObject); begin {запоминание позиции курсора) SPos:=RichEditl-SelStart; with FindDialogl do begin (начальное значение текста поиска — текст, выделенный в RichEdi tl} FindText:=RichEditl.SelText; {позиционирование окна диалога внизу RichEditl) Position:” Point(Forml.Left, Forml.Top+RichEditl.Top+ RichEditl.Height); (удаление из диалога кнопок "Вверх", "Вниз", "Только ' слово целиком") Options:=Options + [frHideUpDown,frHideWholeWord]; {выполнение) Execute; procedure TForml.FindDialoglFind(Sender: TObject); begin with FindDialogl do begin if frMatchCase in Options (поиск с учетом регистра) then RichEditl.SelStart:=Pos(FindText, Copy(RichE- ditl .Lines .Text, SPos+1, Length(RichE- ditl .Lines.Text)))+Spos-1 (поиск без учета регистра) else RichEditl-SelStart:=Pos(AnsiLowerCase(FindText), AnsiLowerCase(Copy(RichEditl.Lines.Text,SPos+1, Length(RichEditl.Lines.Text))))+Spos-1; if RichEditl.SelStart>=Spos then begin {выделение найденного текста) RichEditl.SelLength:=Length(FindText); (изменение начальной позиции поиска) SPos:=RichEdi 11.SelStart+RichEditl-SelLength+1;
Код текстового ре доктора 341 else if MessageDlg( 'Текст "'+FindText+'" не найден. Продолжать диалог?', mtConfirmation.mbYesNoCancel, 0} о mr¥es then CloseDialog; end; RichEditl.SetFocus; end; procedure TForml.ReplaceDialoglReplace(Sender: TObject); begin if RichEditl. Sei Texto ’1 then RichEditl.SelText:-ReplaceDialogl.ReplaceText; if frReplac'eAll in ReplaceDialogl.Options then ReplaceDialoglFind(Seif) ; end; procedure TForml.AReplaceExecute(Sender: TOb j ect); begin [запоминание позиции курсора} SPos:=RichEditl.SelStart; with ReplaceDialogl do begin {начальное значение текста поиска — текст, выделенный в RichEditl) FindText:=RichEdltl.SelText; {позиционирование окна диалога внизу RichEditl} Position:=• Point(Forml.Left,Forml.Top+RichEditl.Торт- RichEditl. Height) ; {удаление из диалога кнопок "Вверх", "Вниз", "Только слово целиком”) Options:=Options + [frHideUpDown,frHideWholeWordJ; {выполнение} Execute; end; end; procedure TForml.ReplaceDialoglFind(Sender: TObject); begin with ReplaceDialogl do begin if frMatchCase in Options {поиск с учетом регистра} then RichEditl.SelStart:“Pos(FindText, Copy(RichEditl.Lines.Text,SPos+1, Length(RichEditl.Lines.Text}))+Spos-1 (поиск без учета регистра} else RichEditl.SelStart:=Pos(AnsiLowerCase(FindText), AnsiLowerCase(Copy(RichEditl.Lines.Text,SPos+1,
242 Приложении 2 Length(RichEditl.Lines-Text))))+Spos-1; if RichEditl.SelStart>=Spos then begin {выделение найденного текста} RichEditl.SelLength:=Length(FindText); (изменение начальной позиции поиска} SPos:=RichEditl-SelStart+RichEditl.SelLength+1; end else if MessageDlg( 'Текст FindText+'" не найден. Продолжать диалог?', mtConfirmation,mbYesNoCancel,D) О mrYes then CloseDialog; end; RichEditl.SetFocus; end; procedure TForml.AExitExecute(Sender: TObject); begin Close; end; П.2 Код графического редактора Ниже приведен полный код графического редактора. Отдель- ные фрагменты этого редактора описаны в разделах 6.1.4 и 6.1.6. Вид редактора во время работы приведен на рис. 6.11 и 6.12. unit UGrEdit; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, Menus, Buttons, ExtDlgs, Clipbrd; type TForml = class(TForml Imagel: TImage; Image? •- TImage; Image3: TImage; Imaged: TImage; MainMenul: TMainMenu; MFile: TMenuItem; SBBrush: TSpeedButton; N1: TMenuItem;
Код графического редакторе! 243 Undo: TMenuItem; SBColor: TSpeedButton; SBRect: TSpeedButton; MOpent TMenuItem; OpenPictureDialogl: TOpenPictureDialog; SBRectang: TSpeedButton; SBFillRec: TSpeedButton; SBErase: TSpeedButton; SBPen: TSpeedButton; SBLine: TSpeedButton; MSave: TMenuItem; MCut: TMenuItem: MCopyz TMenuItem; MPastez TMenuItem; SavePictureDialogl: TSavePictureDialog; procedure FormCreate(Sender: TObject); procedure Image3MouseDown(Sender:TObj ect; Button:TMouseButton;Shift:TShiftState; X, Y:Integer); procedure UndoClick(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure SBBrushClick(Sender: TObject); procedure Image3MouseMove(Sender:TObj ect; Shift:TShiftState;X,Y:Integer); procedure Image3MouseUp(Sender:TObject; Button:TMouseButton;Shift:TShiftState; X, Y: Integer); • . procedure MOpenClick(Sender: TObject); procedure MCopyClick(Sender: TObject); procedure MPasteClick(Sender: TObject); procedure MSaveClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var Forml: TForml; implementation ($R *.DFM) var BitMap,BMCopy:TBitMap; R,RO:TRect; XO,Y0.X1,Y1:longiht; const RBegin:boolean=false; REnd:boolean^ false; RDrag:boolean= false;
244 Приложений 2 procedure TForml.FormCreate(Sender: TObject); var HW,i:integer; begin Bi tMap:=TBi tMap.Create; {задание свойств кисти основного и вспомогательного цветов} Imagel.Canvas.Brush.Color:=clBlack; ImageZ.Canvas.Brush.Color:=clWhite; (заполнение окон основного и вспомогательного цветов} with Imagel.Canvas do FillRect(Rect(0,0,Width,Height)); with ImageZ.Canvas do FillRect(Rect(0,0,Width,Height)); {задание ширины элемента палитры цветов} HW:=Image4-Width div 10; {закраска элементов палитры цветов} with Image4.Canvas do for i:=l to 10 do begin case i of 1:Brush-Color:=clBlack; 2:Brush.Color:=clAgua; 3:Brush.Colort=clBlue; 4:Brush.Color:=clFuchsia; 5:Brush.Color:=clGreen; 6 : Brush . Color: >=clLinie ; 7:Brush.Color:=clMaroon; 8:Brush.Color:=clRed; 9:Brush.Color:=clYellow; 10:Brush.Color:=clWhite; end; Rectangle((i-1)*HW,0,i*HW,Height); end; {рисование креста на холсте — только для тестирования} with Image3 do begin Canvas.MoveTo(0, 0) ; Canvas.LineTo(Width,Height); Canvas.MoveTo(0,Height); Canvas.LineTo(Width,0); end; BitMap.Assign(Image3.Picture); end; procedure TForml.Image3MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if{(Sender as TImage).Name=’Image4’) or SBColor.Down then {режим установки основного и вспомогательного цветов} begin if (Button=mbLeft) then with Imagel.Canvas do begin {установка основного цвета)
245 Brush.Color:=(Sender as TImage).Canvas-Pixels{X,X); FillReet(Rect(0,0,Width,Height)); end else with Image2.Canvas do begin {установка вспомогательного цвета) Brush .Color:=(Sender as TImage).Canvas.Pixels(X,Y); FillReet(Rect(0, 0,Width,Height)); else with Image3.Canvas do begin X0:=X; Y0:=Y; if SBPen.Down then begin {режим карандаша) HoveTo(X,Y); Pen.Color:=Imagel-Canvas.Brush.Color; end else if SBLine.Down then begin {режим линии) X1:=X; Yl:-Y; Pen.Mode:=pmNotXor; Pen.Color:=Image1.Canvas.Brush.Color; end else if SBBrush.Down then begin {режим закраски указанной области холста} if Button=nibLeft then Brush.Color:=Imagel.Canvas.Brush.Color else Brush.Color:=Image2.Canvas.Brush.Color; FloodFill(X,Y,Pixels[X,Y],fsSurface); end else if SBErase.Down then begin {режим ластика) JR:=Rect (X-6, Y-6, X+6, Y+6) ; DrawFocusRect(R); Brush.Color:=Image2-Canvas.Brush.Color; FillReet(Rect(X-5,Y-5,X+5,Y+5)); end else if SBRect.Down or SBRectang.Down or SBFillRec Down then begin {режим работы с фрагментом) if REnd then begin {стирание прежней рамки) DrawFocusRect(R); if (XCR.Right) and (X>R.Left) and <¥>R.Top) and (Y<R.Bottom) {режим начала перетаскивания фрагмента) then begin
246 Приложение 2 {установка флагов} RDrag:=true; REnd:=false; {запоминание начального положения перетаскиваемого фрагмента} RO.TopLeft:=R.TopLeft; RO - BottomRight: =R. Bot tOnfRlght; {запоминание изображения} BitMap.Assign(Image3.Picture); {установка цвета кисти) Brush.Color:=Image2.Canvas.Brush-Color; MCopy.Enabled:=false; MCut.Enabled:=false; end; end else begin {режим начала рисования рамки фрагмента} RBegin:=true; REnd:=false; R.TopLeft:=Point(X,Y); R.BottomRight:=Point(X, Y); DrawFocusRect(Rj: end; end; end; end; procedure TForml-UndoClick(Sender: TObj ect); begin Image3.Picture-Assign(BitMap); end; procedure TForml.FormDestroy(Sender: TObject); begin BitMap.Free; end; procedure TForml.SBBrushClick(Sender: TObject); begin if (Sender as TSpeedButton).Down then BitMap-Assign(Image3.Picture); RBegin :=felse; RDrag:=false; REnd:=false; end; procedure TForml.Image3MouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin
Код гроф* if not (ssLeft in Shift} then exit; {режим линии! if SBLine.Down then with ЬпадеЗ.Canvas do begin {стирание прежней линии) MoveTo1X0,YO); LineTo(XI,Yl); {рисование новой линии) MoveTo(XO,YO) ; LineTo(X, Y); {запоминание новых координат конца линии) XI:=Х; Y1:=Y; end else if SBPen.Down then ЬпадеЗ.Canvas.LineTo(X,Y) else if SBErase.Down then with ЬпадеЗ.Canvas do begin {режим ластика) DrawFocusRect(R); R:=Rect(X-6,Y-б,X+6, Y+6); DrawFocusRect(R); FillReet(Rect(X-5,Y-5,X+5,Y+5)); end else if (SBRect.Down and (RBegin or RDrag)) or SBRectang.Down or SBFillRec.Down then with ЬпадеЗ.Canvas do begin if RBegin then begin (Режим рисования рамки фрагмента) DrawFocusRect(R); if XO<X then begin R.Leftx=X0; R.Right:=X end else begin R.Left:=X; R.Rightx=XO end; if YO<Y then begin R.Topt=YO; R.Bottom:=Y end else begin R.Top:=Y; R.Bottom:=Y0 end; DrawFocusRect(R); end else if SBRect.Down then begin {Режим перетаскивания фрагмента) {восстановление изображения под перетаскиваемым фра гментом) CopyRect(R,BitMap.Canvas,R); {если не нажата клавиша Ctrl — стирание изображения в R0) if not (ssCtrl in Shift) then FillReet{RO); (формирование нового положения фрагмента ) R.Left:=R.Left+X-XO; R.Right:=R.Right+X-XO; R.Top:=R.Top+Y-YO; R_ Bottom:=R.Bot tom+Y-YO; {запоминание положения курсора мыши] ХО:=Х;
YO:=Y; (рисование фрагмента в новом положении} CopyRect(R,BitMap-Canva s,RO); {рисование рамки} DrawFocusReet(R); end; procedure TForml-Image3MouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState: X, Y: Integer); begin with Image3.Canvas do begin if SBLine.Down then begin MoveTo(XO,¥0)4 LineTo(Xl,Yl) ; Pen.Mode:=pmCopy; MoveTo(XD,Y0) ; LineTo(X,Y) ; else if SBRect.Down then begin if RDrag then DrawFocusReet(R); if RBegin and not REnd then begin REnd:=true; MCopy.Enabled:=true; MCut.Enabled:-true; end else if SBRectang.Down then begin Brush.Color:=Imagel.Canvas.Brush-Color; FrameRect(R); end else if SBFillRec.Down then begin Brush.Color:=Image2.Canvas.Brush.Color; Pen.Color:=Imagel-Canvas.Brush.Color; Rectangle(R.Left,R-Top,R.Right,R.Bottom); end else if SBErase.Down then Image3-Canvas-DrawFocusReet(R); RBegin:=false; RDrag:=false; procedure TForml.MOpenClick(Sender: TObject); begin
Код графического редактора 249 if OpenPictureDialogl.Execute then begin Image3.Picture.LoadFromFile( OpenPictureDialogl.FileName); BitMap.Assign(Image3.Picture); end; end; procedure TForml.MCopyClxck(Sender: TObject); begin Image}.Canvas.DrawFocusRect{R): BMCopy:=BltMap-Create; BMCopy.Width:=R.Right-R.Left; BMCopy.Height:=R.Bottom-R.Top; try BMCopy.Canvas.Copyrect(Rect(0,0, BMCopy.Width, BMCopy.Height) , Image3.Canvas,R); ЬпадеЗ.Canvas.DrawFocusRect(R); ClipBoard.Assign(BMCopy); if (Sender as TMenuItem).Name=1MCut' then begin Image3.Canva s.Brush.Color:=clWhite; Image3.Canvas.FillRect(R); end; finally BMCopy.Free; end; end; procedure TForml. MPasteClick{Sender: TObj ect); begin BMCopy:=BitMap.Create; try try BMCopy.LoadFromClipBoardFormat(cf_BltMap, ClipBoard.GetAsHandle(cf_Bitmap) , 0); Image3.Canvas.CopyRect(Rect( 0,0,BMCopy.Width,BMCopy.Height), BMCopy.Canvas,Rect( 0,0,BMCopy.Width,BMCopy.Height) ) ; finally BMCopy.Free; end; except On ElnvalidGraphic do ShowMessage( 'Ошибочный формат графики’); end; end; procedure TForml.MSaveClick(Sender: TObject); begin
If SavePictureDialogl.Execute then begin BitMap.Assign(Image3.Picture); BitMap.SaveToFile(SavePictureDialogl.FileName); end; end; к end-