Текст
                    Д. А. Сурков К.А. Сурков
A.H. Валье ачев
\	\ PFl
ПРОГРАММИРОВАНИЕ
BORLAND
для WINDOWS
«ВЫШЭЙШАЯ ШКОЛА»
Все для профессионала:
IDE
Object Windows
GDI
Resource Workshop
MDI
DLL
DDE
Drag-and-Drop
Turbo Debugger


ББК 22.18я2 С90 УДК 681.3.06:800.92(035) Сканирование, обработка, перевод в формат DJVII Hoza © 01 февраля 2011 года Все права на электронный вариант книги принадлежат за- конным владельцам Электронная версия предоставлена только в ознакомитель- ных целях 6810300600 - 058 С----------------Б398 - 95 М304(03)-96 ISBN 985-06-0162-0 © Д.А. Сурков, К.А. Сурков, А.Н. Вальвачев, 1996
ПРЕДИСЛОВИЕ Вот уже несколько лет подряд популярность операционной системы Windows растет с невероятной быстротой. Сейчас, наверное, уже нет ни одной серьезной программы, которую бы не перевели с MS-DOS на Windows. Более того, когда законченной версии Windows 95 еще не было в продаже, ряд фирм уже объявили о создании для нее новых версий своих лучших систем: Lotus Development - 1-2-3 и Ami Pro, Novell - WordPerfect, Microsoft - Excel, Word и PowerPoint, Oracle - 32-разрядной версии Oracle Forms. Эти факты говорят о том, что лю- бой уважающий себя программист должен научиться создавать про- граммы для системы, на которую постепенно переходит весь компью- терный мир. Однако дело это не простое, часто болезненное, так как требует много времени и упорства. Зато результат превосходит даже самые смелые ожидания. Количество пособий по программированию для Windows на рус- ском языке, к сожалению, крайне незначительно. Полезная информа- ция разбросана по журналам, компьютерным Не1р’ам, переводам с англоязычных изданий с немалым числом ошибок как в терминоло- гии, так и в исходных текстах программ. Авторы данного издания попытались помочь начинающим про- граммистам в трудном деле освоения Windows и собрали в одной кни- ге все, что необходимо для самостоятельного написания Windows- приложений достаточно высокого качества. В основу книги положен опыт авторов в области разработки программ для Windows и некото- рые результаты, полученные при работе со студентами в высших учебных заведениях Беларуси, России и Украины. Внимание уделяется как типовым задачам, так и достаточно сложным и практически не описанным в литературе вопросам. Книга рассчитана на тех, кто уже знаком с языком Turbo Pascal и объектно-ориентированным программированием. Все примеры отла- жены и выполнены под управлением Borland Pascal 7.0 with Objects и новейшей Pascal-ориентированной системы Delphi. В настоящее время основными языками для создания Windows- приложений являются Borland Pascal и C++, поэтому, не зная этих языков, нельзя считать себя профессиональным и, что немаловажно, перспективным программистом. Авторы долго не могли решить, ка- кой из этих двух языков принесет больше пользы отечественным про- граммистам, и остановились ца Borland Pascal. Это решение принято по следующим достаточно серьезным причинам: • Borland Pascal значительно легче и быстрее понимается студен- тами, чем C++, поэтому его изучают первым в большинстве вузов как европейских, так и других стран;
4 Программирование в среде Borland Pascal для Windows • с точки зрения языковых возможностей Borland Pascal практиче- ски ничем не уступает C++; • количество пользователей Borland Pascal как в СНГ, так и за границей намного превосходит количество работающих на C++; • по скорости компиляции Borland Pascal превосходит известные реализации C++ в десятки раз, что превращает отладку программы из мучения в удовольствие; • требования, предъявляемые к знанию аппаратуры при работе с Borland Pascal, значительно ниже, чем при работе с C++; • знание Borland Pascal является необходимым для быстрого ос- воения новейшей и наиболее перспективной в настоящий момент сис- темы программирования Delphi. Методическая основа книги оригинальна и с успехом применялась авторами при написании книг для англо- и испаноязычных студентов’ и программистов различного уровня подготовки. Примеры обсужда- лись с рядом многоопытных программистов из США и Германии с целью максимального приближения читателя к “боевой обстановке”, чтобы он свободно ориентировался в наиболее актуальных проблемах программирования и мог с успехом решать их как для отечественных заказчиков, так и для всех прочих. Позволим себе дать несколько советов по изучению Windows и Borland Pascal с помощью данного пособия: • бегло просмотрите всю книгу и хотя бы в общих чертах уясните назначение каждой из глав, отбрасывая для начала все непонятное; • чтение теоретического материала должно сопровождаться посто- янным изучением текстов программ, несмотря на то, что некоторые из них с первого взгляда могут показаться начинающему просто “неподъемными”; • изучив главу полностью, постарайтесь написать и отладить не- большую программу, основанную на текущем материале, но содер- жащую что-то новое и по возможности близкое к вашей тематике. Авторы надеются, что представленный в книге материал окажет читателям практическую помощь и позволит каждому желающему быстро выйти на достаточно высокий уровень программирования. В заключение авторы выражают признательность сотрудникам Европейского отделения фирмы Borland International Рону Энтропу и Люку Вермюлену - за консультации по новейшим методам програм- мирования, Ю.С. Бородичу - за конструктивную критику и ряд цен- ных советов по содержанию и оформлению книги. Все замечания и пожелания просим направлять по адресу: 220048, Минск, проспект Машерова, 11, издательство “Вышэйшая школа”. Авторы
1 ИНТЕГРИРОВАННАЯ СРЕДА РАЗРАБОТЧИКА 1.1. ИНСТАЛЛЯЦИЯ И ЗАПУСК Итак, вы купили Borland Pascal with Objects 7.0 и стали зарегистриро- ванным пользователем продуктов фирмы Borland. В коробке находят- ся книги и дискеты. С книгами все понятно - это руководства для пользователя по эксплуатации и самым разным вопросам, касающим- ся программирования. На дискетах размещена система Borland Pascal with Objects 7.0, которая содержит весь необходимый программисту инструментарий, включая три интегрированных среды разработчика (Integrated Development Environment - IDE). Именно с помощью IDE программа набирается, компилируется и выполняется. Возникает вопрос: зачем так много сред? Ответ прост: для удовлетворения за- просов пользователей, имеющих компьютеры различной мощности и работающих с различными операционными системами. Рассмотрим кратко назначение каждой из сред: • IDE Turbo Pascal 7.0 (файл TURBO.EXE) предназначена для ра- боты в реальном режиме операционной системы MS-DOS на всех компьютерах, оснащенных микропроцессором 80x86. Напомним, что реальный режим (DOS real mode) позволяет использовать объем па- мяти не более 640 К, то же ограничение накладывается и на размер компилируемой программы; • IDE Borland Pascal (файл ВР.ЕХЕ) работает в защищенном режи- ме (DOS protected mode) на компьютерах, оснащенных микропроцес- сором 80286 или выше и расширенной памятью (extended memory) не менее 2 Мбайт. Позволяет создавать очень большие программы как для защищенного и реального режимов MS-DOS, так и для Windows; • IDE Borland Pascal for Windows (файл BPW.EXE) работает в среде Windows, служит для разработки Windows-приложений и программ для реального и защищенного режимов MS-DOS. В настоящее время компьютерный мир переживает переходный пе- риод, когда практически все программисты работают как под MS- DOS, так и под Windows, поэтому вам может пригодиться каждая из перечисленных оболочек. Наша книга посвящена проблемам про-
6 Программирование в среде Borland Pascal для Windows граммирования для Windows, поэтому наибольшее внимание будет уделено интегрированной среде BPW. Прежде всего необходимо инсталлировать (перенести) систему Borland Pascal with Objects 7.0 с инсталляционных дискет на жесткий диск (HD) вашего компьютера. Это делается в среде MS-DOS сле- дующим образом: • установите инсталляционный диск 1 в дисковод А:; • наберите команду A:\INSTALL и нажмите клавишу Enter; • следуя подсказкам, поочередно установите в дисковод А: все ин- сталляционные дискеты; • закончив инсталляцию, внесите в файл CONFIG.SYS инструк- цию FILES = 20, а в файле AUTOEXEC.BAT укажите путь к установ- ленной системе: PATH = C:\BP\BIN. Вот и все. На жестком диске вашего компьютера установлены все три IDE. Теперь можно приступать к работе с Borland Pascal for Windows (для краткости обозначим его как BPW). Но прежде необхо- димо освоить работу с интегрированной средой, которая сделает про- цесс написания программы не только эффективным, но и приятным. 1.2. ГЛАВНОЕ МЕНЮ Интегрированная среда BPW запускается из Windows выбором соот- ветствующей пиктограммы в Диспетчере Программ (Program Manager). После старта IDE на экране появится главное окно (рис. 1.1). В верхней части окна располагается главное меню в виде горизон- тальной полосы (menu bar). С его помощью выполняются все необхо- димые для разработки программы действия, включая написание, от- ладку и выполнение. Начинающим программистам не следует удив- ляться большему числу различных режимов и окон. Поработав час с IDE, вы поймете красоту и “железную” логику построения меню, жизненную необходимость каждого из его многочисленных режимов. Кратко опишем назначение основных команд меню: File - чтение, запись и другие манипуляции с файлами; Edit - редактирование текста в одном или нескольких окнах; Search - поиск и замена заданной строки в тексте; Run - выполнение программы; Compile - компиляция программы из активного окна; Tools - запуск вспомогательных (служебных) программ; Options - установка режимов функционирования IDE; Window - манипуляции с окнами; Help - вызов справочника (Help’a) по IDE и языку Borland Pascal. Ниже полосы меню находится инструментальная панель (Toolbar) которую также называют скоростной панелью (SpeedBar). Размещен- ные на ней кнопки дублируют часто используемые команды меню и
Гпава 1. Интегрированная среда разработчика 7 Рис. 1.1. Главное окно IDE BPW ускоряют доступ к соответствующим операциям. Если в IDE нет от- крытых окон редактирования, то в инструментальной панели доступ- ны только команды Help-Contents, File-Open..., File-Exit. В нижней части окна находится панель состояния (Status Ваг). При выборе команды из меню или инструментальной панели в ней ото- бражается краткая характеристика команды. При наборе текста про- граммы панель состояния отображает информацию о текущей пози- ции курсора (строка, колонка), режим набора текста - вставка (Insert) или перезапись (Overwrite), а сразу после компиляции еще и информа- цию об ошибке, если таковая была обнаружена. Центральная часть окна представляет собой рабочую область, ко- торая используется для окон текстового редактора и ведения диалога с программистом на протяжении всего сеанса работы с IDE. Обратиться к любой команде главного меню можно одним из трех способов. Можно нажать и отпустить клавишу Alt или F10 и с помо- щью клавиш перемещения курсора выбрать необходимую команду, которая тут же будет выделена соответствующим цветом. Для выпол- нения команды надо нажать клавишу Enter. Второй способ предпола- гает использование мыши. Достаточно установить курсор мыши на нужную команду, щелкнуть левой кнопкой, и команда будет выпол- нена. Третий способ заключается в обращении к “горячим” клавишам (hot keys). В ключевом слове каждой команды выделена одна буква (“горячая”), как правило, заглавная. Нажав одновременно клавишу
8 Программирование в среде Borland Pascal для Windows Alt и клавишу с любой из выделенных букв, можно сразу выполнить соответствующую команду. Например, команду Compile можно вы- брать, нажав клавиши Alt+C (знак “+” обозначает нажатие комбина- ции указанных клавиш, начиная с первой). Исключение, составляет переход к системному меню, где “горячими” клавишами вызова будут Alt+Spacebar. Вы вправе выбрать для себя любой вариант управления меню, но, по нашему мнению, удобнее всего работается с мышью. Так как ниже мы будем много говорить о манипуляторе мышь, да- вайте сразу определимся с некоторыми выражениями, которые будем использовать: установить мышь - значит поместить курсор мыши в нужную точку экрана; нажать кнопку мыши - нажать левую кнопку манипулятора, как стандартно зарезервированную для управления; выбрать мышью - установить курсор в заданную точку экрана и на- жать левую кнопку. Если по каким-либо причинам мышь недоступна, программист может ускорить работу с IDE, обращаясь к акселераторным клавишам, нажатие которых вызывает немедленный старт часто используемых команд меню. Например, за командой File-Open... закреплена акселе- раторная клавиша F3, за командой File-Save - клавиша F2, а за ко- мандой File-Exit - комбинация клавиш Alt+X. Отметим, что это со- глашение о закреплении клавиш действует только тогда, когда уста- новлено множество “родных” команд среды BPW. Установить систе- му команд можно в окне Preferences, которое создается по команде меню Options-Environment-Preferences... . В этом окне нужно нажать кнопку мыши на соответствующем поле в группе Command Set. Если выбрать мышью поле с пометкой CUA (Common User Access), то бу- дет установлено множество команд системы Windows. В этом случае принятое в BPW закрепление клавиш действовать не будет. Если чи- татель знаком с предыдущими версиями компилятора BPW, то лучше установить альтернативное множество команд (пометить в группе Command Set поле Alternate). Это множество собственных команд среды BPW, о которых мы и будем вести речь. 1.3. УПРАВЛЕНИЕ ОКНАМИ Одной из наиболее впечатляющих особенностей интегрированной среды BPW является мощная оконная система. Окно - это ограничен- ная рамкой: область экрана, выполняющая функции целого экрана. После открытия окна остальная область экрана для ввода-вывода как бы не существует. Окно можно переместить, изменить его размеры, перекрыть другими окнами, закрыть. Оконный интерфейс признан наиболее удобным среди всех других видов интерфейсов, поэтому он стал стандартом и присутствует во всех Windows-приложениях. Окно,
Глава 1. Интегрированная среда разработчика 9 Рис. 1.2. Окно редактора текстов которое отображается на экране при старте IDE, является главным. В главном окне можно открывать другие окна, в частности окна ре- дактора (рис. 1.2). На рис. 1.2 показаны два окна редактирования, открытые в глав- ном окне IDE. Очевидно, что только одно из них может быть актив- ным в данный момент. Активное окно обозначается цветовым выделе- нием его заголовка. Кроме заголовка, каждое окно имеет системное меню (в левом верхнем углу окна), кнопки минимизации и максимизации (в правом верхнем углу окна), полосы горизонтальной и вертикальной прокрутки текста (соответственно внизу и справа). С помощью сис- темного меню окном можно управлять с клавиатуры. Можно изме- нить его местоположение (команда Move), размер (Size), свернуть в пиктограмму (Minimize), восстановить оригинальный размер (Restore), раскрыть на всю область основного окна (Maximize), закрыть (Close), перейти к следующему окну (Next). Команда Minimize соответствует кнопке минимизации, a Maximize - кнопке максимизации. Эти кнопки могут выбираться с помощью мыши. Полосы горизонтальной и верти- кальной прокрутки (скроллинга) служат для просмотра текста и управляются с помощью мыши. Для управления расположением окон и переключением между ок- нами в главном окне используется меню Window. Окна могут распола- гаться либо с наложением друг на друга (Cascade), либо в одной плос- кости (Tile). Если некоторые окна свернуты в пиктограммы, то навес- ти порядок, изменив их расположение, можно с помощью команды
10 Программирование в среде Borland Pascal для Windows Arrange icons. Закрыть сразу все окна позволяет команда Close all в меню Window. В нижней части меню Window располагаются заголовки открытых окон (имена редактируемых файлов). Эти пункты добав- ляются в меню динамически по мере открытия окон и служат для бы- строго перехода к нужному тексту. Одной из разновидностей окон являются так называемые окна диалога (dialog boxes). Это стандартное средство системы Windows в интегрированной среде BPW используется для управления работой различных команд (например, с помощью окон диалога устанавлива- ются значения опций, рабочие каталоги). Для всех строк меню, где после имени команды идет подразумевается наличие своего окна диалога. Например, на рис. 1.3 показано окно диалога, создаваемое по команде меню Search-Replace... (Заменить). Окно диалога всегда находится в активном состоянии и этим отли- чается от обычных окон. Очевидно, что при появлении активного окна на экране продолжить работу можно только после его закрытия. Что и делается мышью или нажатием клавиши Esc. Внутри окна диалога предусмотрено шесть основных типов управ- ляющих элементов: © поле действия (кнопка button); © поле проверки (кнопка независимой фиксации check box); • поле выбора (кнопка зависимой фиксации radio button); к • строка или блок редактора (edit); • блок списка (list box); text to find: Hew text: Replace Text begim begin Options: Direction: ♦ Forward Backward Change Z Case sensitive Whole words only J Regular expression И Prompt on replace I. Scope: Origin: ♦ Global ^elected text f From cursor > Entire scope Д^Сапсе! Рис. 1.3. Окно установки параметров для поиска и замены текста
Гпава 1. Интегрированная среда разработчика 11 • комбинированный блок, состоящий из строки редактора и блока списка (combo box). В окне диалога все элементы управления могут быть отнесены к од- ному из указанных типов. Несколько связанных между собой по смыслу элементов управления могут объединяться в группы, которым присваиваются имена. Например, на рис. 1.3 выделены^группы: Text to find, New text, Options, Scope, Direction и Origin. Переход от одной группы к другой можно выполнить с помощью мыши либо нажимая клавиши Tab или Shift+Tab. Рассмотрим на примере окна диалога Replace Text указанные выше элементы управления. На рис. 1.3 показаны четыре поля действия', продолжить (Ок), изменить все (Change all), завершить (Cancel) и вызвать помощь (Help). Выбор любого из них приводит к выполнению определенных дейст- вий. Так, полеОк зарезервировано для фиксации внесенных в диало- говом окне изменений и перехода к выполнению команды. Поле Cancel предназначено для прекращения диалога и отмены команды с игнорированием всех изменений. После выбора поля Help открывает- ся окно справки по элементам управления текущего окна диалога. Перечисленные поля выполняют эти стандартные действия для всех окон диалогов. Назначение оставшегося поля Change all специфично только для окна диалога Replace. Нажатие клавиши Enter аналогично выбору поля Ок. На рис. 1.3 показана группа из четырех полей проверки с наимено- ванием Options (Опции). Любое другое окно диалога может содержать большее или меньшее количество полей проверки в зависимости от того, насколько это необходимо для выполнения команды. Поле про- верки всегда используется для задания условий со строго ограничен- ным выбором начальных значений: активно или не активно. Обозна- чение [V] в поле проверки указывает на то, что данное условие нахо- дится в активном состоянии. Например, в окне диалога Replace Text установлено условие отличия прописных и строчных букв в тексте (Case sensitive). Когда группа полей проверки на экране активна, то перемещение по строкам можно выполнить с помощью клавиш пере- мещения курсора либо мыши. Если требуется изменить состояние конкретного поля проверки, следует нажать клавишу Spacebar или “горячую” клавишу, соответствующую подчеркнутой букве в наиме- новании условия. Поля выбора в отличие от полей проверки предназначены для зада- ния условий с большим количеством взаимоисключающих значений, при этом все значения должны уместиться в заданном окне диалога. На рис. 1.3 показаны две группы полей выбора: область действия (Scope) и точка отсчета (Origin). Каждая группа отвечает за установку только одного условия, наименование которого вынесено в заголовок группы. Текущее значение в группе обозначено на экране как <•>.
12 Программирование в среде Borland Pascal для Windows Если требуется установить новое значение, следует активизировать группу и сделать в ней свой выбор с помощью мыши, клавиш переме- щения курсора либо выделенных букв. В некоторых случаях текст для задания конкретного условия вы- полнения команды вводится с клавиатуры. Для этого в окне диалога предназначено специальное редактируемое поле. Если справа от него расположена кнопка [4<], то это указывает на возможность обращения к списку предыдущих значений данного поля. В этом списке запомина- ются все, строки, которые когда-либо были введены в поле ввода и к которым можно при необходимости обратиться. Благодаря этой удобной возможности легко избавиться от ненужной практики повто- рения ввода с клавиатуры ранее введенных строк. Строка ввода вместе со списком возможных значений образует но- вый элемент управления, который называется комбинированным блоком. На практике комбинированные блоки используются намного чаще, чем одиночные строки ввода. Например, на рис. 1.3 представлен комбинированный блок Text to find (Текст для поиска) и показана строка begim, которую необходимо найти в тексте. Она заменяется на введенную вами другую строку, например begin. Ввод каждой из этих строк завершается нажатием клавиши Tab, после чего выполняется переход к следующему полю окна диалога. Еще одним элементом управления в окне диалога является блок списка. Представление его на экране очень напоминает уже известный список предыдущих значений, с той лишь разницей, что он всегда отображен на экране. Часто блок списка организуется для выбора значения и^ пополняемого множества альтернатив. Текущее значение в списке выделяется соответствующим цветом. Чтобы активизировать блок списка, достаточно выбрать мышью любой его участок либо воспользоваться клавишей Tab. На экране в строке с текущим значе- нием появится курсорная рамка, перемещая которую с помощью мыши либо клавиш, можно выбрать любое новое значение. Если все воз- можные значения списка не помещаются на экране, то обычными средствами управления можно легко организовать его просмотр. 1.4. ВСТРОЕННЫЙ РЕДАКТОР ТЕКСТОВ 1.4.1. ОКНО РЕДАКТИРОВАНИЯ В процессе создания программ важнейшую роль играют средства ра- боты с исходным текстом. Обычно они сосредоточены в специальной подпрограмме, которая называется текстовым редактором. В BPW редактор встроен в IDE, что позволяет мгновенно переходить от ре- дактирования к компиляции и выполнению. Редактирование несколь- ких файлов одновременно, снятие ограничений с объема редактируе-
Гпава 1. Интегрированная среда разработчика 13 Рис. 1.4. Окно ввода имени файла мых файлов, выделение ключевых слов другим шрифтом и цветом, возможность отмены сделанных изменений, интеграция с другими составными частями компилятора - вот далеко не полный перечень возможностей редактора. Чтобы войти в редактор, достаточно выбрать команду меню File- New (Новый файл). На экране появится пустое окно с именем nonameOO.pas. Курсор установлен в верхнюю левую позицию окна. Теперь можно вводить текст программы с помощью клавиатуры. Чтобы открыть еще одно окно (для другой программы) достаточно повторить вызов команды New. Если требуется отредактировать ранее созданный и сохраненный на диске файл, то используется команда File-Open... (Открыть файл). При этом на экране появится окно File Open (рис. 1.4), в котором зада- ется имя нужного файла. Ввод имени заканчивается нажатием клави- ши Enter или кнопки мыши на поле Ок. 1.4.2. КОМАНДЫ РЕДАКТОРА При описании команд редактора будем использовать следующие по- нятия; символ, слово, строка, страница, блок и файл. “Символ”, “строка” и “файл” соответствуют известным и привычным читателю терминам. Слово представляет собой последовательность символов, отделенную с обеих сторон пробелами или специальными символами. Страница формируется из строк текста, количество которых не пре-
14 Программирование в среде Borland Pascal для Windows восходит высоты окна редактирования. Блоком называется помечен- ный специальным образом участок текста любых размеров. На экране он выделяется цветом, отличным от цвета остального текста. Большинство команд редактирования соответствуют аналогичным командам в других популярных текстовых редакторах, поэтому вам вряд ли придется переучиваться. Начнем с команд перемещения курсора. Они обеспечивают быстрое перемещение курсора по экрану, строке, блоку и программе в целом (табл. 1.1). Клавиши выбраны таким образом, что после небольшой тренировки каждый сможет работать с ними вслепую без всяких про- блем. Если читатель плохо знает клавиатуру, полезно будет порабо- тать с одной из многочисленных программ, специально предназна- ченных для изучения клавиш (например, с TypeTutor). Табл. 1.1. Команды перемещения курсора Действие <—, —> Перевести курсор на символ влево или вправо т, г На строку вверх или вниз Ноте В начало строки End В конец строки PgUp, PgDn На страницу вверх или вниз Ctrl + <- На слово влево Ctrl + -> На слово вправо Ctrl+Home В начало окна Ctrl+End В конец окна Ctrl+PgUp В начало файла Ctrl+PgDn В конец файла Ctrl+Q+B В начало блока Ctrl+Q+K В конец блока Ряд весьма полезных клавиш управляет вводом символов с клавиа- туры. Ввод текста осуществляется в двух основных режимах: вставки (Insert) и перезаписи (Overwrite). В режиме вставки введенный символ как бы раздвигает строку вправо на одну позицию от места положе- ния курсора. В режиме перезаписи сдвиг не выполняется и новый сим- вол записывается на место старого, затирая его. Смена режимов вы- полняется нажатием клавиши Ins. Соответствующая режиму надпись (Insert или Overwrite) появляется на панели состояния. Все остальные команды управления предназначены для корректировки уже подго- товленного текста (табл. 1.2). Они говорят сами за себя и дополни- тельных комментариев не требуют.
Гпава 1. Интегрированная среда разработчика 15 Табл. 1.2. Команды вставки и удаления Команда Действие / Ins Del Backspace Ctrl+Y Ctrl+Q+Y Ctrl+N Режим вставки «включить-выключить» Удалить символ в позиции курсора Удалить символ слева от курсора Удалить строку Удалить символы от курсора до конца строки Вставить строку При редактировании часто возникает ситуация, когда определен- ные участки текста повторяются в одном или нескольких файлах. Очевидно, что несколько раз вводить с клавиатуры один и тот же текст было бы просто неразумно. Для разрешения таких ситуаций предназначены команды манипуляции блоками текста (табл. 1.3). Табл. 1.3. Команды работы с блоками Команда Действие Shift+Del Ctrl+Ins Shift-bins Ctrl+Del Ctrl+K+C Ctrl+K+Y Ctrl+K+V Ctrl+K+H Ctrl+K+P Ctrl+K+R Ctrl+K+W Перенести блок в буфер обмена Скопировать блок в буфер обмена Принять блок из буфера обмена Удалить блок Скопировать блок Удалить блок Переместить блок Скрыть или отобразить блок Печатать блок Читать блок с диска Записать блок на диск Прежде всего определите блок: установите курсор на начало вы- бранного участка текста и нажмите комбинацию клавиш Ctrl+K+B, затем переведите курсор на предполагаемый конец блока и нажмите Ctrl+K+K. После этого заданный блок будет выделен отличным от остального текста цветом. Для определения блока можно еще исполь- зовать комбинацию клавиш Shift+?, >1, <- или мышь с нажатой (от начала до конца блока) кнопкой. После выделения блока к нему при- меняются команды, приведенные в табл. 1.3. С их помощью можно эффективно управлять блоками в текущем окне редактирования: пе- ремещать блок с одного места на другое, копировать, удалять, сохра- нять блок на диске и т.д.
16 Программирование в среде Borland Pascal для Windows Блок можно переносить из одного окна редактирования в другое. Механизм перемещения основан на использовании специальной промежуточной области - так называемого буфера обмена, который еще называют “карманом”. Буфер обмена (Clipboard) - это зарезерви- рованная область памяти (общая для всех, редактируемых текстов), через которую производится обмен блоками данных между окнами интегрированной среды и между различными прикладными програм- мами. Чтобы скопировать блок текста из одного окна в другое, нужно последовательно выполнить следующие действия: отметить блок в первом окне и нажать клавиши Ctrl+Ins для помещения блока в Clipboard, затем с помощью клавиш Ctrl+Tab перейти во второе окно и нажать Shift+Ins для копирования блока из буфера обмена в теку- щее окно, после чего операция переноса будет завершена. Для любого редактора, включая BPW, характерно наличие команд быстрого перемещения в определенное место текста. В одном случае это может быть просто поиск (Find) заданной строки текста, в дру- гом - поиск и замена (Replace), в третьем - переход к определенной строке редактирования по ее номеру (Go to line number). Очевидно, что все эти операции требуют ввода дополнительной информации, на базе которой нужная позиция и вычисляется. При выборе команды Search-Find... (Найти) или нажатии Ctrl+Q+F на экране отобразится окно диалога Find Text, поля которого задают режимы поиска (рис. 1.5). После ввода искомой строки и установки Рис. 1.5. Окно установки параметров для поиска текста
I лава 1. Интегрированная среда разработчика 17 фебуемых режимов поиска в окне диалога выбирается поле Ок. Например, на рис. 1.5 заданы следующие условия поиска: требуется найти слово begin с различием строчных и прописных букв, поиск начать с позиции, где установлен курсор, и далее до конца файла. Когда искомое слово найдено, оно выделяется соответствующим цветом, а курсор устанавливается за последней его буквой. Чтобы продолжить поиск с заданными условиями, следует нажать Ctrl+L или выполнить команду Search-Search again (Поиск опять). Поиск осуще- ствляется до тех пор, пока не достигнут конец файла. Команда Search-Replace... в отличие от команды Search-Find... не только находит заданную строку, но и заменяет ее на другую. В усло- вия выполнения команды добавлены строка замены и несколько опций (см. рис. 1.3), в частности необходимость подтверждения каж- дой операции замены (Prompt on replace). Если выбрать мышью поле Change all (Изменить все), то операция замены будет автоматически повторяться до тех пор, пока искомая строка встречается в тексте. Если для выполнения команды Search-Replace... используется обычное поле Ок или клавиша Enter, то для последующего выполнения коман- ды с теми же условиями следует нажать клавиши Ctrl+L. Команда Search-Go to line number (Перейти к строке по ее номеру) гребует ввода номера строки, на которую нужно установить курсор. Обработав введенную цифру, система выводит на экран фрагмент текста с искомой строкой. С помощью команды Undo (Отменить) в меню Edit можно отме- нить внесенные в текст изменения. Команда Redo (Восстановить после отмены) отменяет действия, выполненные командой Undo. Нажатие клавиш Alt+X завершает работу с IDE. 1.5. ПЕРВАЯ ПРОГРАММА Сценарий создания любой программы всегда один и тот же. Исходный текст вводится с помощью клавиатуры и сохраняется на диске. Затем с целью поиска возможных синтаксических ошибок выполняется ком- пиляция программы. Найденные ошибки исправляются. Когда на жране появляется сообщение об успешной компиляции, можно при- ступать к выполнению программы. На этой стадии могут появиться ошибки, вызванные неточностями алгоритма. Только после их ис- правления процесс разработки можно считать законченным. На при- мере элементарной программы покажем, как этот сценарий реализу- ется практически. Стартуем IDE и открываем окно редактора с помощью команды New меню File или нажатием клавиш Alt+F+N. Набираем текст программы:
18 Программирование в среде Borland Pascal для Windows program First; uses WinCrt; var Name: String; begin WritefKaK Вас зовут?'); Readln(Name); Writeln(Name,поздравляю с первой программой !'); end. Программа крайне проста, но для начинающих мы дадим необхо- димые комментарии. Прежде всего, обратите внимание на то, что операторы программы разделяются точкой с запятой, а в самом конце текста программы ставится точка. Первая строка содержит заголовок программы, который записывается после служебного слова prograpi. Вообще говоря, этой строки в тексте может и не быть, но хороший стиль программирования предполагает наличие заголовка в про- грамме. После оператора uses следует список подключаемых к про- грамме системных модулей. Они имеют расширение TPW и содержат библиотеки стандартных процедур и функций, которые использует как система BPW, так и программист. Важнейший модуль имеет имя SYSTEM и подключается к программе системой BPW автоматически, поэтому его не надо указывать в списке uses. Он жизненно необходим любой программе. Активизация всех остальных системных модулей требует указания их имен в списке uses. В программе FIRST подключен только модуль WINCRT. Этого вполне достаточно, так как текст элементарен. После запуска про- граммы WINCRT формирует на экране окно, с помощью которого вводятся данные и выводятся результаты. Ввод данных выполняют операторы Read и Readin, а вывод - Write и Writein. Отметим, что несмотря на то, что в Pascal-приложениях для операционной системы MS-DOS операторы Read, Readln, Write, Writein организуют стан- дартный ввод-вывод, в Pascal-приложениях для Windows они практи- чески не применяются, хотя это и возможно. Их работу обеспечивает модуль WINCRT; если его не подключить, то после запуска програм- мы на экране просто ничего не появится. Дело в том, что прикладная Windows-программа должна быть построена по определенным прави- лам, которые существенно отличаются от правил построения MS- DOS-программ. К тому же, Windows - это графическая среда, и обыч- ные средства ввода-вывода, ориентированные на файлы, напрямую в ней использоваться не могут. Модуль WINCRT имитирует работу в MS-DOS. Его подключение обеспечивает создание на экране окна и работу многих процедур и функций, часто используемых в MS-DOS, например процедур Read, Readln, Write, Writein. Имитация применя- ется, как правило, в двух случаях: при написании программ, рабо- тающих только в тестовых режимах (без графики), в которых требо-
I пава 1. Интегрированная среда разработчика 19 вания к интерфейсу с пользователем не являются определяющими, и при переносе уже существующих MS-DOS-программ в Windows. Сле- дует принимать во внимание, что далеко не все DOS-программы на языке Pascal могут быть перенесены в Windows без изменений. Это < вязано с тем, что Windows является многозадачной средой, а в мно- । озадачных операционных системах должны соблюдаться определен- ные правила построения прикладных программ. Правила построения настоящих” Windows-приложений будут рассмотрены в гл. 2. Однако вернемся к программе FIRST. Слово var открывает секцию объявления используемых в программе переменных. Объявлена толь- ко одна переменная Name типа String, которая позволяет использо- на гь для имени Name 255 символов. Тело программы составляют операторы, которые находятся между операторными скобками begin и end. Первый оператор программы Write выводит на экран предложение ’Как Вас зовут?’. Затем процеду- ра Readln читает введенное пользователем с клавиатуры имя, которое запоминается в переменной Name. Последний оператор выводит на жран значение Name вместе с искренним поздравлением. При наборе программы FIRST вы можете столкнуться с тем, что в интегрированной среде Borland Pascal for Windows не отображаются русские буквы. Это происходит по двум причинам: либо на компью- । ере не установлена программа-драйвер для работы с русским алфа- витом (программа-русификатор), либо в интегрированной среде ис- пользуется шрифт, в котором отсутствуют_русские буквы. В первом i лучае мы советуем вам обзавестись одной из программ русификации, с реди которых наиболее распространены CyrWin и ParaWin. Во вто- ром случае попробуйте заменить шрифт, используемый при наборе инета. Для этого командой меню Options-Environment-Editor... от- кройте окно Editor и выберите в поле комбинированного блока с мет- кой Font шрифт с русскими буквами. Еще одна особенность использования русских букв состоит в том, •но их расположение в кодовой таблице символов, принятой в Windows, и альтернативной кодовой таблице MS-DOS не совпадает. В результате текст, набранный в MS-DOS и состоящий из символов с номерами, превышающими 127, в Windows выглядит как набор непо- нятных значков. То же самое получается при просмотре в среде MS- I )()S текста, подготовленного в Windows и состоящего из символов с номерами больше 127. Однако не стоит огорчаться, так как в состав BPW входит программа перекодировки файлов из одной таблицы в другую. Она находится в директории C:\BP\EXAMPLES\WIN\OWL и называется FCONVERT.PAS. Мы советуем ее откомпилировать и результат поместить в группу Borland Pascal оболочки Program Manager. В этой программе конвертирование из OEM в ANSI означа- ет преобразование символов кодовой таблицы MS-DOS в символы
20 Программирование в среде Borland Pascal для Windows кодовой таблицы Windows, а конвертирование из ANSI в OEM - об- ратное действие. Для сохранения исходного текста используется команда Save (Сохранить) меню File (акселераторная клавиша F2). Если окно ре- дактора было открыто командой File-New, то при первом сохранении на экране отображается окно, в котором пользователю предлагается ввести имя нового файла. После ввода имени следует нажать клавишу Enter или выбрать мышью поле Ок. Новый файл с заданным именем будет сформирован в указанном каталоге, после чего изменится поло- са заголовка у окна редактирования. Если не вводить новое имя, то файлу по умолчанию (автоматически) присваивается стандартное имя nonameOO.pas. Команда Save as... (Сохранить как...) позволяет сохранить один и тот же файл под разными именами. Команда Save all (Сохранить все) сохраняет сразу все редактируемые файлы. После того, как программа сохранена, можно смело переходить к этапу компиляции. Компилятор проверит ваш текст на наличие син- таксических ошибок и выполнит множество других необходимых операций. Для начала компиляции нажмите клавиши Alt+F9 или вы- берите в меню команду Compile-Compile. На экране появится окно Compiling (Компиляция), которое додержит информацию о программе и ходе процесса компиляции. Если в тексте программы обнаружена ошибка, то на панель со- стояния выводится соответствующее сообщение об ошибке с указанием ее возможной причины. В этом случае курсор автоматически устанав- ливается либо в строку, приведшую к ошибке, либо в следующую за ней. Далее текст такой программы необходимо исправить и выпол- нить команду Compile еще раз. Если ошибок больше нет, то компиля- тор создает образ выполнимого (executable) файла, который имеет рас- ширение EXE и готов к автономному (без IDE) выполнению непо- средственно из Windows. После любой корректировки следует сохранить текст программы с помощью команды File-Save и перейти к самой интересной части ра- боты - выполнению программы и исследованию результатов. Чтобы выполнить программу, следует нажать клавиши Ctrl+F9 либо активизировать команду Run (Выполнить) в меню Run. Перед выполнением система автоматически проверяет, проводилась ли ком- пиляция программы. Если таковой не было, то программа сначала автоматически компилируется, а потом уже выполняется. На нор- мальный ход выполнения программы может повлиять наличие ошибок времени выполнения (run-time errors). В этом случае Windows выводит на экран соответствующее сообщение и досрочно завершает про- грамму, в которой возникла такая ошибка. После внесения соответст- вующих изменений программу следует опять отправить на выполне-
I пава 1. Интегрированная среда разработчика 21 и не. Этот процесс продолжается до тех пор, пока не будет устранена последняя ошибка. С помощью команды Open... (Открыть) меню File (акселераторная клавиша F3) можно загрузить в окно редактора текст любой про- । раммы из имеющихся на диске. Во время выполнения команды на жране появится список всех файлов в текущем каталоге. С помощью мыши или клавиатуры можно легко выбрать любой файл. По умолчанию в IDE предусмотрено автоматическое сохранение на диске списка всех редактируемых файлов после завершения сеанса работы. Если это происходит, то в начале следующего сеанса ваша последняя программа также автоматически будет загружена в память. Теперь немного о печати. В меню File имеется очень полезная ко- манда Print (Печатать). Выбрав ее, вы распечатаете текст из активно- ю окна редактора. Кстати, не забудьте перед этим включить принтер и вставить в него бумагу. Выбор и настройка принтера осуществля- ю1ся командой Printer setup. В IDE BPW существует удобное средство просмотра (browsing) ис- пользуемых в программе объектов, модулей, а также глобальных пе- ременных, процедур и функций. Соответствующие окна просмотра ткрываются по командам Objects, Units и Globals в меню Search. Эти возможности требуют хорошего знания языка и объектно- ориентированного программирования и предназначены для опытных пользователей. Главное меню содержит ряд утилит, к которым вы будете обра- щаться по мере накопления опыта. Они размещены в меню Tools и выполняют множество необходимых программисту операций: Turbo Debugger - внешний (по отношению к BPW) отладчик. Это < чинственное средство отладки Windows-приложений; Resource Workshop - редактор ресурсов. Что такое ресурсы и как «»ни используются в программе, вы узнаете несколько позже; WinSight - удобная вспомогательная утилита для анализа работы Windows-приложений; Turbo Profiler - исследует программу с целью улучшения скорост- ных характеристик. Для запуска любой из перечисленных утилит достаточно выбрать (ч‘ имя в меню с помощью клавиатуры или мыши. В процессе работы с IDE у начинающих, да и у профессионалов, moi ут возникнуть самые различные вопросы. Для ответа на них об- ращайтесь к всезнающему помощнику - системе контекстно- ывисимой помощи (Online Context-Sensitive Help). Это делается про- । io выберите в меню Help требуемую команду или нажмите соответ- < шующую этой команде акселераторную клавишу. На экране появит- »я окно Borland Pascal Help (рис. 1.6), посредством которого можно ici ко добраться до любой интересующей вас темы
22 Программирование в среде Borland Pascal для Windows Borland Pascal Help File Edit Bookmark Help £ontenhr[ Search j Вуюк { History | << » Contents To get Help on an item, click the icon or underlined text To get Help on Help press Fl Essential and concepts you'll need to know for working with Borland Pascal. Language Reference for Borland Pascal including errormessages, sample programs and other utilities. TasM with step-by-step directions for using Borland Pascal and its utilities Meru Command? and dialog box reference. KeyhocHd-shortcuts and equivalents. Shortcut: Use the Search button at the top of the Help window to find Help topics Рис. 1.6. Окно справочника no Borland Pascal Перечислим команды меню Help: Contents (Shift+Fl) - открыть оглавление справочника; Topic search (Ctrl+Fl) - дать информацию о слове (команде, опера- тору), на которое указывает в данный момент курсор; Using help - дать информацию по использованию системы контек- стно-зависимой помощи; Compiler directives - дать справку по директивам компилятора; Procedures and Functions - дать справку по подпрограммам языка Borland Pascal; Reserved words - вывести список зарезервированных слов; Standard units - показать имена стандартных модулей BPW; Turbo Pascal language - дать справку по языку Pascal; Error messages - дать список сообщений об ошибках. Для получения справки по режимам нажмите клавишу F1. Отметим, что окно, изображенное на рис. 1.6, принадлежит от- дельной программе, которая запускается по командам из меню Help и может присутствовать в памяти вместе с интегрированной средой. Вы можете переключаться на систему помощи точно так же, как переклю- чаетесь в среде Windows на другие программы. Для закрытия окна Borland Pascal Help достаточно нажать клавиши Alt+F4 или в систем- ном меню окна выбрать команду Close.
i пава 1. Интегрированная среда разработчика 23 Вся справочная информация в системе помощи структурирована на отдельные взаимосвязанные темы (topics), которые образуют иерархию. Если тема не умещается в границах окна, то ее можно про- » мотреть с помощью клавиш управления курсором или полос скрол- ।IIига, которые в этом случае создаются в окне. В тексте, относящемся к одной теме, могут встречаться слова или < иовосочетания, выделенные зеленым цветом и подчеркиванием. Это . сылки, предназначенные для перехода к другим, как правило сопутст- вующим, темам. Когда на ссылку попадает курсор мыши, он из с грелки” превращается в “перст указующий”. Для перехода к ука- 1.ШН0Й “перстом” теме, нужно один раз щелкнуть мышью на соответ- . 1вующей ссылке или установить с помощью клавиш Tab и Shift+ТаЬ курсорную рамку на требуемое слово и нажать Enter. Нажав кнопку мыши на поле Contents (Содержание), можно перей- । и к теме самого верхнего уровня иерархии, которая обобщает содер- кание других тем. Выбор поля Search приводит к отображению окна, в котором в ал- фавитном порядке собраны заголовки всех тем. Быстрый поиск в спи- »ке задается нажатием букв необходимого ключевого слова. После набора темы окно Search закрывается и происходит переход к этой имев окне Borland Pascal Help. Для возврата от текущей темы к предыдущей следует нажать кноп- ку мыши на поле Back (Назад). После выбора кнопки History (Предыстория) появляется окно, где и обратном порядке перечислены темы, из которых вы попали в дан- ную тему. Это так называемый стек тем. Для возврата в любую из предыдущих тем достаточно выбрать ее имя в списке History. Поля « и » в окне Borland Pascal Help служат для просмотра тем одного уровня иерархии. Если при работе с системой помощи появятся какие-либо вопросы, • Тратитесь к команде меню Using help (Как работать с системой по- мощи) или два раза нажмите клавишу F1. На экране появится под- робная инструкция по эксплуатации Help’a. Вот, пожалуй, и все, что нужно знать начинающему программисту о своем могучем помощнике - IDE BPW. Мы предполагаем, что чита- ।еиь знаком с основами языка Borland Pascal (типами данных, опера- юрами, объектами и т.д.), и сразу приступаем к изучению програм- мирования под Windows.
2 ОСНОВЫ ПРОГРАММИРОВАНИЯ В WINDOWS 2.1. СОГЛАШЕНИЯ WINDOWS API С точки зрения программиста, Windows представляет собой набор подпрограмм, обращаясь к которым можно относительно просто создать эффектный интерфейс со всеми присущими Windows атрибу- тами (окнами, кнопками, полосами скроллинга и т.д.). Все процедуры и функции операционной среды, вызываемые при- кладными программами во время работы, называются интерфейсом прикладной программы - API (от англ. Application Programming Interface). Windows API представлен в Borland Pascal совокупностью TPW-модулей (см. прил. А), среди которых выделим следующие: • WINTYPES - включает определения констант и типов данных; • WINPROCS - предоставляет доступ к стандартным процедурам и функциям Windows; • WIN31 - предоставляет доступ к расширенным возможностям Windows 3.1. Программный интерфейс Windows включает более 800 функций, запомнить их крайне сложно, поэтому программист должен знать способ их записи (нотацию), чтобы по имени понять назначение функции или переменной. Нотация - это соглашение об именовании функций, переменных и констант в некоторой системе. Имена функций Windows построены по принципу “глагол-существительное”, например: CreateWindow (Создать окно), LoadMenu (Загрузить меню), SendMessage (Послать сообщение). В основе имен переменных и параметров лежит венгерская нотация, которая названа так в честь родины ее создателя Charles Simonyi. В венгерской нотации имена переменных и параметров функций содержат префикс, описывающий тип данных или указы- вающий на способ использования переменной либо параметра. В ка- честве примера рассмотрим следующие имена: var IpszFileName: PChar; ciCount: Integer; cwSize: Word;
Гпава 2. Основы программирования е Windows 25 Проанализируем имена: IpszFileName - переменная-указатель на нуль-терминированную строку, предназначенную для хранения имени файла (префикс Ipsz означает “Long Pointer to String terminated by Zero”); ciCount - переменная типа Integer, в которой хранится число элементов некоторого массива (префикс ci означает “Counter of Integer type”); cwSize - переменная, задающая размер некоторого бло- ка данных (cw - “Counter of Word type”). Список часто используемых префиксов приведен в табл. 2.1. Табл. 2.1. Префиксы имен Префикс Описание а b by с ch dw fn i 1 Массив (array) Булевское (boolean) Байт (byte) Счетчик (counter) Символ (char) Двойное слово (double word) Функция (function) Целое (integer) Длинное (long) n Целое число (number) P s Указатель (pointer) Строка (string) sz Строка формата ASCIIZ w Слово (word) X Координата X У Координата Y Тот же принцип используется и при именовании констант, но в ка- честве префикса берется группа принадлежности константы. В част- ности, имена оконных сообщений имеют префикс wm_ (от англ. Window Message), например: wm_Char, wm_Close, wm_Quit. Для на- чинающих “расшифровка” имен бывает достаточно сложна, но по мере накопления опыта вы будете делать это автоматически. И еще одно важное замечание. Процедуры и функции Windows ис- пользуют не строки языка Pascal типа String, а нуль-терминированные строки языка С. Нуль-терминированная строка представляет собой индексированный от нуля массив ASCII-символов, заканчивающийся символом #0 (zero). Отсюда и ее второе название - ASCIIZ-строка. Для поддержки ASCIIZ-строк в Borland Pascal введен тип данных PChar, объявленный как указатель на Char. Работа со строками типа
26 Программирование е среде Borland Pascal для Windows PChar (копирование, слияние, сравнение и т.д.) предполагает исполь- зование специальных процедур и функций модуля STRINGS (см. ру- ководство по BPW). 2.2. ОБЪЕКТЫ И ДЕСКРИПТОРЫ Создание любых Windows-приложений базируется на объектной тех- нологии. Прикладные программы, пользуясь услугами Windows, соз- дают различные объекты и управляют ими. Типичными объектами являются окна, меню, пиктограммы, диалоговые блоки, перья и кисти для рисования, таймер. Даже выполняемая пользовательская про- грамма рассматривается как объект системы Windows. Очевидно, что,работая со множеством объектов, Windows должна как-то отличать их друг от друга. Эта проблема решается с помощью дескрипторов. Дескриптор (handle) представляет собой 16-разрядное слово, однозначно определяющее объект в среде Windows. Тип деск- риптора определен в модуле WINTYPES: type THandle = Word; Дескриптор можно рассматривать как индекс (номер) объекта в некоторой системной таблице объектов. Он всегда передается первым параметром в функцию Windows, работающую с этим объектом. В MS-DOS аналогом дескриптора Windows-объекта является дескрип- тор файла. Использование дескрипторов позволяет скрыть от прикладного программиста детали внутренней организации Windows, что дает возможность создателям Windows совершенствовать систему изнутри, не требуя изменения кода прикладных программ. Адресация объектов с помощью дескрипторов не накладывает никаких ограничений на аппаратное обеспечение, что резко повышает мобильность операци- онной системы. 2.3. ОКНА Само название системы - Windows - говорит о том, что важнейшими объектами в ней являются окна самых различных типов. С точки зре- ния пользователя окно - это прямоугольная область экрана, предна- значенная для ввода и вывода информации. Каждая программа имеет одно или несколько окон, одно из которых является главным. При запуске программы главное окно появляется на экране первым и вы- дает пользователю список доступных команд - меню. Закрытие глав- ного окна автоматически вызывает завершение работы программы.
Гпава 2. Основы программирования в Windows 27 г- Кнопка системного меню г Заголовок окна L Горизонтальная полоса скроллинга Кнопка минимизации Кнопка максимизации Вертикальная полоса скроллинга Рис. 2.1. Элементы окна Окна имеют ряд стандартных элементов, основные из них показа- ны на рис. 2.1. Большинство окон, создаваемых в рамках одной программы, зави- сят друг от друга. Взаимоотношения между ними строятся по принци- пу “родитель-ребенок”. Родительское окно может владеть одним или несколькими дочер- ними окнами. Дочерние окна всегда располагаются поверх родитель- ского окна и закрываются вместе с ним. Родительское окно может быть дочерним окном другого окна, выступающего для него роди- тельским. У окна может быть несколько дочерних окон, но только одно родительское. Если окно создается без родителя, Windows на- значает ему в качестве владельца всю рабочую область экрана (desktop). Не следует путать отношение “родитель-ребенок” между окнами с отношением наследования в объектно-ориентированном программи- ровании. Дочернее окно не происходит от родительского окна и не наследует его свойства; отношение “родитель-ребенок” является не более чем отношением владения: окна-родители владеют и управляют своими детьми. С точки зрения программы окно представляет собой объект Windows, управляемый с помощью дескриптора. Внутренняя структу- ра окна в памяти скрыта от программиста, но он может управлять им (создавать, перемещать, уничтожать и т.д.) с помощью специального набора функций Windows.
28 Программирование в среде Borland Pascal для Windows 2.4. СОБЫТИЙНОЕ УПРАВЛЕНИЕ Нетрудно догадаться, что управлять окнами и множеством других объектов программы - задача не из простых. Для ее решения в Windows используется механизм обработки событий. Под событием понимается факт свершения элементарного дейст- вия, от которого может зависеть ход выполнения программы. Собы- тиями, например, являются: нажатие клавиши на клавиатуре, переме- щение мыши, истечение заданного промежутка времени. Программа, ориентированная на обработку событий, имеет архитектуру, пока- занную на рис. 2.2. Рис. 2.2. Программа с событийно-управляемой архитектурой Сразу после инициализации программы управление передается на цикл обработки событий, который автоматически выполняет ряд опе- раций: получение события, проверку условия выхода из программы,
Гпава 2. Основы программирования е Windows 29 диспетчирование события соответствующей процедуре обработки. Под диспетчированием понимается выбор нужной подпрограммы обработки события. При поступлении события, указывающего на необходимость завершить программу, происходит выход из цикла обработки событий и управление передается на процедуру деинициа- лизации. Достоинство такой схемы управления в ее универсальности: чтобы в существующую программу добавить реакцию на новое событие, не нужно менять существующий исходный код. Достаточно определить новую процедуру, которая при возникновении этого события будет осуществлять необходимые действия. Событие в системе Windows характеризуется: • получателем (тот, кому адресовано это событие); • типом, который связан с источником события; • временем возникновения; • положением на экране курсора мыши в момент возникновения события; • в зависимости от типа, событие может характеризоваться рядом дополнительных параметров, интерпретация которых специфична для каждого конкретного случая. 2.5. СООБЩЕНИЯ Все события, возникающие в системе Windows, приводятся к одному общему виду, называемому сообщением. Сообщение - это запись определенной структуры, которая содер- жит исчерпывающую информацию о происшедшем событии. Струк- тура сообщения в Windows имеет следующий вид: TMsg = record hwnd: HWnd; message: Word; wParam: Word; IParam: Longint; time: Longint; pt: TPoint; end; Поясним назначение полей. Поле hwnd содержит 16-разрядный де- скриптор окна, в котором сообщение возникло. В поле message поме- щается двухбайтный код (тип) сообщения, выделяющий данное сооб- щение из более чем 120 имеющихся в Windows. Стандартные сообще- ния описаны в справочнике по Borland Pascal. Программист может также определять свои собственные сообщения. Поля w Param и IParam содержат дополнительную информацию и зависят от типа
30 Программирование е среде Borland Pascal для Windows сообщения. В сообщениях, определяемых пользователем, они могут использоваться для передачи любой необходимой информации. В поле time система Windows помещает время в миллисекундах, которое истекло с момента запуска системы до постановки сообщения в оче- редь. Поле pt указывает позицию курсора мыши в экранных коорди- натах на момент возникновения события. Сообщения могут поступать в программу от многочисленных ис- точников: • пользователь генерирует сообщения, нажимая клавиши на кла- виатуре или перемещая мышь и нажимая ее кнопки; • сама система Windows может посылать сообщения прикладной программе для уведомления о тех или иных событиях; • программа может вызывать функции Windows, результатом ко- торых является посылка сообщений программе; • прикладная программа может посылать сообщения самой себе; • прикладная программа может посылать сообщения другим при- кладным программам. Столь большое количество возможных сообщений требует нали- чия стройной системы их обработки. 2.6. СХЕМА ОБРАБОТКИ СООБЩЕНИЙ Схема обработки сообщений достаточно сложна, поэтому мы рас- смотрим ее на примере работы Windows с клавиатурой и мышью (рис. 2.3). События, генерируемые клавиатурой, мышью или другим устрой- ством, обрабатываются соответствующим драйвером и помещаются в системную очередь сообщений. Системная очередь в Windows одна, в ней может одновременно находиться до 30 сообщений. Из системной очереди сообщения распределяются по приложени- ям. Для каждого приложения Windows организует отдельную очередь сообщений прикладной программы, которая по умолчанию вмещает до 8 сообщений. В процессе распределения сообщений по приложениям Windows извлекает очередное сообщение из системной очереди, опре- деляет, с каким окном оно связано, и помещает в очередь того прило- жения, которому принадлежит окно. Как Windows определяет адреса- та, ведь клавиатура и мышь являются разделяемыми устройствами, а значит, их сообщения могут предназначаться любому приложению? Ответ таков: Windows допускает выполнение одновременно несколь- ких приложений, каждое из которых может иметь множество окон. Однако только одно окно в каждый отдельный момент времени может получать сообщения от клавиатуры. Принято говорить, что это окно имеет фокус ввода. Фокус ввода может быть изменен, например, с по- мощью мыши (для этого достаточно щелкнуть мышью в требуемом
Гпава 2. Основы программирования е Windows 31 Приложение А Приложение В Рис. 2.3. Схема обработки сообщений от клавиатуры и мыши окне). Приложение, окно которого имеет фокус ввода, называется активным. Сообщения от клавиатуры помещаются в очередь активно- го приложения. Сообщения от мыши обрабатываются по-другому, они передаются тому приложению, в окне которого находится указа- тель мыши. Обработку прикладной очереди сообщений осуществляет уже само приложение. Для этого программа организует так называемый цикл обработки сообщений. В нем осуществляется выбор нового сообщения из очереди прикладной программы и вызов диспетчера для его обра- ботки соответствующей функцией (в данном случае А или В). В связи с тем, что сообщения связаны с окнами, функции обработки сообще- ний называют оконными функциями. На этом мы заканчиваем описание базовых понятий Windows и переходим непосредственно к программированию.
32 Программирование в среде Borland Pascal для Windows 2.7. WINDOWS-ПРОГРАММА 2.7.1. “ПРОСТАЯ” ПРОГРАММА Напишем простейшую программу, которая строит типичное для Windows-приложений окно: program MinWin; { Подключение модулей Windows-API. Условное подключение модуля MESSAGES обеспечивает компиляцию программы в среде Delphi} uses WinTypes, WinProcs {$ifdefVer80}, Messages {$endif}', { Оконная функция для обработки сообщений } function WindowProc(HWindow: HWnd; Message, WParam: Word; LParam: Longint): Longint; export; begin WindowProc := 0; case Message of { wm_XXXX: обработка различных сообщений } wm_Close: DestroyWindow(HWindow); wm_Destroy: PostQuitMessage(O); else WindowProc := DefWindowProc(HWindow, Message, WParam, LParam); end; end; const WndClassName = ’SampleWindowClass’; var HWindow: HWnd; Msg: TMsg; WndClass: TWndClass; begin { Если запущен первый экземпляр программы, то выполняем регистрацию нового оконного класса с именем ,SampleWindowClass,} if HPrevInst = 0 then begin with WndClass do begin style := 0; IpfnWndProc := ©WindowProc; cbCIsExtra := 0; cbWndExtra := 0; hlnstance := System.HInstance; hlcon := Loadlcon(0, idi_Application); hCursor := LoadCursor(0, idc_Arrow); hbrBackground := GetStockObject(white_Brush); IpszMenuName :="; IpszClassName := WndClassName; end;
Гпава 2. Основы программирования в Windows 33 if not RegisterClass(WndClass) then Halt(255); end; { На основе класса 'SampleWindowClass' создаем окно программы } HWindow := CreateWindow( WndClassName, 'Hello, Windows!', ws_OverlappedWindow, cwJJseDefault, cwJJseDefault, cwJJseDefault, cwJJseDefault, 0, 0, Hlnstance, nil); { Отображаем окно на экране; CmdShow - вид окна при отображении } ShowWindow(HWindow, CmdShow); { Обновляем содержимое окна } UpdateWindow(HWindow); { Организуем цикл обработки сообщений } while GetMessage(Msg, 0, 0, 0) do begin { Транслируем клавиатурные сообщения в символьные } Т ranslateMessage(Msg); {Диспетчируем полученное сообщение } DispatchMessage(Msg); end; {При получении сообщения wm_Quit выходим из программы} Halt(Msg.wParam); { Msg.wParam - код завершения в сообщении wm_Quit} end. { имя оконного класса } { заголовок окна } { стиль окна } { координаты левого верхнего угла } { ширина и высота окна } { дескриптор родительского окна } { дескриптор меню } { дескриптор копии приложения } { указатель на дополнительные данные } У вас, наверное, уже возникли вполне обоснованные сомнения в том, является ли эта программа простейшей. Если она имеет такой объем и столь устрашающий вид, то как будет выглядеть более слож- ная программа? Увы, это действительно элементарная программа и этап ее понимания необходимо пройти каждому начинающему про- граммисту. Откомпилируйте и запустите программу MINWIN. На экране поя- вится окно с заголовком ’Hello, Windows!’, кнопками минимизации и максимизации, системное меню, вызываемое по кнопке в левом верх- нем углу окна. Окно можно перемещать по экрану, изменять в разме- рах, свернуть в пиктограмму, раскрыть на весь экран, закрыть. При- нимая во внимание все эти богатые возможности, мы бы назвали про- грамму MINWIN даже компактной. Перечислим основные действия, выполняемые программой MINWIN: • регистрация оконного класса; • создание окна; • отображение окна; • обновление рабочей области окна; 2 Зак. 1049
34 Программирование в среде Borland Pascal для Windows • организация цикла обработки сообщений. Цикл обработки сообщений включает: • прием сообщения; • трансляцию сообщения; • диспетчирование сообщения. Реакцию программы на поступающие сообщения описывает окон- ная функция, которая вызывается из диспетчера. 2.7.2. РЕГИСТРАЦИЯ ОКОННОГО КЛАССА Окно в Windows создается на основе оконного класса. Оконный класс представляет собой запись с основными свойствами всех окон этого класса. В оконном классе задаются, например, форма курсора в рабо- чей области окна, необходимость автоматического обновления окна при изменении его размеров, адрес процедуры обработки сообщений. В Windows существует несколько стандартных классов окон с заранее заданными свойствами. Однако каждое приложение, как правило, регистрирует свой собственный оконный класс с тем, чтобы можно было управлять всеми свойствами создаваемого окна. Для создания оконного класса требуется объявить запись типа TWndClass, заполнить ее поля соответствующими значениями и пере- дать в виде параметра функции RegisterClass, т.е. зарегистрировать ее в Windows. Тип данных TWndClass определен в модуле WINTYPES: TWndClass = record style: Word; IpfnWndProc: TFarProc; cbCIsExtra: Integer; cbWndExtra: Integer; hlnstance: THandle; hlcon: Hlcon; hCursor: HCursor; hbrBackground: HBrush; IpszMenuName: PChar; IpszClassName: PChar; end; Поясним назначение полей записи: • style - битовые флаги, задающие начальные свойства всех окон, создаваемых на основе оконного класса. Возможные стили оконного класса приведены в табл. 2.2. Для установки и комбинирования сти- лей следует использовать операцию or. Для отмены стиля применяется операция and not; • IpfnWndProc - указатель на callback-функцию, вызываемую для обработки сообщений, адресованных окнам этого класса. Callback- функция отличается от обычной тем, что вызывается извне приклад- ной программы (в данном случае самой системой Windows);
Гпава 2. Основы программирования в Windows 35 Табл. 2.2. Стили оконного класса Константа Описание cs_VRedraw Вызывает в окне перерисовку всего содержимого при изменении вертикального размера окна. Если выво- димое в окне изображение не зависит от высоты окна, то данный стиль можно не устанавливать в оконном классе cs_H Redraw Вызывает в окне перерисовку всего содержимого при изменении горизонтального размера окна. Если выводимое в окне изображение не зависит от шири- ны Окна, то данный стиль можно не устанавливать в оконном классе csJDblClks Позволяет окну реагировать на двойные щелчки мыши cs_OwnDC Указывает, что каждый экземпляр окна должен иметь свой собственный контекст отображения (контекст отображения используется для вывода информации в окно) cs_ClassDC Указывает, что оконный класс должен иметь свой собственный контекст отображения, разделяемый всеми окнами, принадлежащими данному классу cs_ParentDC Если данный стиль установлен, то окно использует контекст отображения родительского окна cs_NoClose cs_ByteAlignClient Запрещает в системном меню окна команду Close Выравнивает рабочую область окна на границе бай- та видеопамяти. Может ускорить графический вывод в окно cs_ByteAlignWindow Выравнивает окно на границе байта видеопамяти. Может ускорить перерисовку окна cs_GlobalClass Указывает на то, что оконный класс является гло- бальным и может использоваться всеми работаю- щими приложениями • cbClsExtra - дополнительное количество байт, выделяемое за структурой оконного класса (по умолчанию cbClsExtra = 0); • cbWndExtra - дополнительное количество байт, выделяемое для экземпляра окна (по умолчанию эта память инициализируется нулем); • hlnstance - дескриптор экземпляра приложения, регистрирующе- го оконный класс; • hlcon - дескриптор пиктограммы, отображаемой на экране при минимизации окна. В Windows существует много предопределенных пиктограмм, которые вы можете использовать в своей программе. Для создания своих собственных пиктограмм служит программа
36 Программирование в среде Borland Pascal для Windows Resource Workshop, которая входит в поставку Borland Pascal 7.0. Работа с редактором Resource Workshop рассматривается в гл. 5; • hCursor - дескриптор курсора, который рисуется в окне при ото- бражении окна на экране. В Windows существует несколько предопре- деленных видов курсора - стрелка, песочные часы, двойная стрелка. С помощью программы Resource Workshop вы можете создавать свои собственные формы представления курсора в окне; • hbrBackground - дескриптор кисти, которая используется для за- крашивания фона окна; • IpszMenuName - идентификатор меню, загружаемого из ресурса прикладной программы. Это меню используется для окна по умолча- нию, если при его создании функцией CreateWindow не указано другое меню; • IpszClassName - строковый идентификатор оконного класса в Windows. Когда все поля записи TWndClass заполнены соответствующими значениями, производится регистрация оконного класса с помощью функции RegisterClass(var WndClass: TWndClass): Bool; .которая возвращает целочисленное значение, однозначно идентифи- цирующее оконный класс в системе. Программист должен считаться с тем, что Windows разрешает иметь в памяти и одновременно выполнять сразу несколько экземпля- ров одной и той же программы, причем каждый экземпляр идентифи- цируется своим дескриптором. Дескриптор экземпляра HInstance оп- ределен в модуле SYSTEM. При запуске нового экземпляра програм- мы ему передается дескриптор уже существующего экземпляра этой программы, который был запущен до него. Дескриптор предыдущего экземпляра HPrevInst тоже определен в модуле SYSTEM. Для первого экземпляра программы HPrevInst = 0. Поэтому запускаемая програм- ма всегда может узнать, имеется ли в памяти ее копия. Это требуется знать при регистрации оконного класса, так как регистрировать оконный класс нужно при запуске только первого экземпляра про- граммы. 2.7.3. СОЗДАНИЕ ОКНА Для создания окна на основе некоторого класса служит функция CreateWindow(ClassName, WindowName: PChar; Style: Longint; X, Y, Width, Height: Integer; WndParent: HWnd; Menu: HMenu; Instance: THandle; Param: Pointer): HWnd; где ClassName - имя оконного класса, в котором содержатся основные
Гпава 2. Основы программирования в Windows 37 свойства создаваемого окна; Window Name - указатель на нуль- терминированную строку, содержащую заголовок окна; Style - стиль окна (оконные стили подробно обсуждаются в гл. 3, поэтому здесь они не приводятся); X, Y, Width, Height - начальные координаты и размеры окна (если X и Width равны cw_UseDefault, то начальные координаты и размеры окна Windows выбирает сама); WndParent - дескриптор родительского окна; Menu - дескриптор устанавливаемо- го в окне меню (если он равен нулю, то используется меню оконного класса); Instance - дескриптор экземпляра программы, создающего окно; Param - указатель на дополнительные данные пользователя. Функция возвращает дескриптор, который используется затем для отображения и обновления окна. В случае неудачи CreateWindow воз- вращает нуль. После выполнения функции CreateWindow окно формируется в памяти, но на экране не отображается. 2.7.4. ОТОБРАЖЕНИЕ ОКНА Для отображения окна на экране используется функция ShowWindow(Wnd: HWnd; CmdShow: Integer): Bool; где Wnd - дескриптор окна, полученный в результате вызова CreateWindow; CmdShow - вид окна при отображении (развернутое, минимизированное и т.д.). Возможные значения параметра CmdShow приведены в табл. 2.3. Функция возвращает True, если до ее вызова окно уже было отображено на экране. В противном случае возвраща- ется False. Табл, 2.3. Значения параметра CmdShow в функции Show Window Константа Описание sw_Hide Окно исчезает и теряет активность sw_ShowNormal, Окно становится активным и отображается на sw_Normal экране. Если до этого окно было минимизирова- но или максимизировано, то его оригинальные координаты и размеры восстанавливаются sw_ShowMinimized Окно становится активным и отображается в виде пиктограммы sw_ShowMaximized, Окно становится активным и отображается мак- sw_Maximize симально раскрытым s w_ShowN о Activate Окно отображается с текущими положением и размерами. Активное в данный момент окно остается активным sw_Show Окно становится активным и отображается с текущими положением и размерами
38 Программирование в среде Borland Pascal для Windows Окончание табл. 2.3 Константа Описание sw_Minimize Окно минимизируется и активность переходит к предыдущему активному окну s w_Sho wN о Activate Окно отображается с текущими положением и размерами. Активное в данный момент окно остается активным sw_ShowNA Окно отображается в текущем состоянии. Актив- ное в данный момент окно остается активным sw_Restore Окно становится активным и отображается на экране. Если до этого окно было минимизирова- но или максимизировано, то его оригинальные координаты и размеры восстанавливаются Отметим, что способ отображения главного окна автоматически передается в программу при ее запуске и задается глобальной пере- менной CmdShow модуля SYSTEM. 2.7.5. ОБНОВЛЕНИЕ РАБОЧЕЙ ОБЛАСТИ ОКНА Для обновления содержимого окна вызывается процедура UpdateWindow(Wnd: HWnd); Она принимает только один параметр - дескриптор окна - и заставля- ет окно перерисовать свою рабочую область, посылая ему сообщение wm_Paint. 2.7.6. ЦИКЛ ОБРАБОТКИ СООБЩЕНИЙ Когда окно создано и отображено на экране, программа начинает обрабатывать сообщения, поступающие от различных источников - клавиатуры, мыши, таймера. Для этого организуется цикл обработки сообщений, который включает извлечение сообщения из прикладной очереди, трансляцию этого сообщения (действие необязательное, но рекомендуемое) и диспетчирование сообщения в соответствующую оконную функцию. Цикл обработки сообщений имеет следующий вид: while GetMessage(Message, 0, 0, 0) do { прием сообщения } begin ТranslateMessage(Message); { трансляция сообщения } DispatchMessage(Message); { диспетчирование сообщения } end;
Гпава 2. Основы программирования в Windows 39 2.7.7. ПРИЕМ СООБЩЕНИЯ Для извлечения сообщений из очереди приложения существуют две функции, объявленные в WINPROCS следующим образом: PeekMessage(var Msg: TMsg; Wnd: HWnd; MsgFilterMin, MsgFilterMax: Word; RemoveMsg: Word): Bool; GetMessage(var Msg: TMsg; Wnd: HWnd; MsgFilterMin, MsgFilterMax: Word): Bool; где Msg - возвращаемое программе сообщение; Wnd - дескриптор окна, для которого ожидается сообщение (передача в этом параметре нуля указывает на то, что извлекается сообщение, предназначенное любому окну прикладной программы); MsgFilterMin, MsgFilterMax - минимальное и максимальное допустимые значения поля message в записи Msg (для получения всех сообщений оба параметра нужно ус- тановить в нуль); RemoveMsg - режим работы функции PeekMessage: pm_NoRemove - принятое сообщение остается в очереди, pm_Remove - сообщение удаляется из очереди. Флаг pm_NoYield используется в комбинации с одним из этих значений, чтобы предотвратить передачу управления другим задачам. Функция PeekMessage является более мощной и универсальной, чем GetMessage. Она просматривает очередь прикладной программы в поисках сообщения, удовлетворяющего условию, заданному пара- метрами Wnd, MsgFilterMin, MsgFilterMax. Если такое сообщение найдено, оно возвращается в программу через параметр Msg. Функ- ция PeekMessage возвращает True, если в очереди найдено сообщение, удовлетворяющее заданному параметрами условию. В противном случае возвращается False. Функция GetMessage является частным случаем PeekMessage. В ней отсутствует параметр RemoveMsg, она всегда удаляет сообщения из очереди и возвращает True, если принятое сообщение не является со- общением wm_Quit (поле message в записи Msg не равно wm_Quit). В сложных программах чаще используется функция PeekMessage, пре- доставляющая программисту более широкие возможности. В нашем простом примере вполне достаточно функции GetMessage. 2.7.8. ТРАНСЛЯЦИЯ СООБЩЕНИЙ Сообщения, поступающие от клавиатуры, требуют дополнительной обработки - трансляции. Поэтому после приема сообщения нужно вызвать функцию TranslateMessage(var Msg: TMsg): Bool, которая преобразовывает сообщение о нажатии клавиши в сообщение
40 Программирование в среде Borland Pascal для Windows о приходе символа. Дело в том, что сообщения, генерируемые драйве- ром клавиатуры, содержат виртуальный key-code, который определя- ет, какая клавиша была нажата, но не определяет символьное значе- ние этой клавиши. Функция TranslateMessage, не изменяя переданное ей сообщение, помещает еще одно сообщение с соответствующим символьным значением в очередь приложения. Функция возвращает True, если переданное ей сообщение оказалось клавиатурным. 2.7.9. ДИСПЕТЧИРОВАНИЕ СООБЩЕНИЙ После трансляции сообщение диспетчируется оконной процедуре, что и делает функция DispatchMessage(var Msg: TMsg): Longint; Результат обработки сообщения возвращается как значение функ- ции. Его смысл зависит от типа обработанного сообщения и чаще всего игнорируется прикладной программой. Цикл обработки сообщений повторяется до получения програм- мой сообщения wm_Quit. В этом случае функция GetMessage возвра- щает False, цикл завершается и происходит выход из программы. 2.7.10. ОКОННАЯ ФУНКЦИЯ Итак, мы определили и создали окно, отобразили его на экране и на- чали обрабатывать поступающие сообщения. В нашем примере их обработку выполняет оконная функция WindowProc. Фактически написание Windows-приложений сводится к разработке оконных функций, которые и определяют поведение программы после ее запус- ка. Формат оконной функции: WindowProc(Window: HWnd; Message, WParam: Word; LParam: Longint): Longint; export; Описание функции включает слово export, что делает ее экспорти- руемой, т.е. доступной для вызова извне (по отношению к прикладной программе). Параметры функции соответствуют полям структуры TMsg и принимаются из сообщения, передаваемого в функцию DispatchMessage. Первый параметр содержит дескриптор окна, кото- рому адресовано это сообщение, второй - код сообщения. Третий и четвертый параметры являются дополнительными и служат для пере- дачи в оконную функцию информации, специфичной для каждого конкретного сообщения. Если нет специальных требований к обраба- тываемому сообщению, то возвращаемое оконной функцией значение может быть произвольным. В рассматриваемом примере оконная функция обрабатывает всего два сообщения Windows: wm_Close и wm_Destroy. Сообщение
Гпава 2. Основы программирования в Windows 41 wm_Close посылается окну в ответ на выбор в его системном меню команды Close или нажатие комбинации клавиш Alt+F4. Оно посыла- ется как сигнал того, что окно должно быть закрыто. При поступле- нии этого сообщения WindowProc вызывает стандартную функцию DestroyWindow для уничтожения окна. Обрабатывая сообщение wm_Close, приложение может попросить у пользователя подтвержде- ния закрыть окно, вызывая DestroyWindow только при положитель- ном ответе пользователя. Например, перед закрытием окна текстово- го редактора пользователю может быть предложено сохранить изме- ненный документ на диске. Функция DestroyWindow удаляет окно со всеми его дочерними ок- нами. При этом окну посылается сообщение wm_Destroy, чтобы оно могло удалить свои рабочие данные. В примере вызов DestroyWindow приводит к удалений) главного (и единственного) окна программы, а это в свою очередь означает завершение выполнения Windows- приложения. Для завершения программы по сообщению wm_Destroy вызывается процедура PostQuitMessage(ExitCode: Integer); которая помещает в очередь прикладной программы сообщение wm_Quit с кодом возврата ExitCode в TMsg.wParam и немедленно возвращается в точку вызова. При очередном вызове GetMessage это сообщение будет получено, функция GetMessage вернет значение False, программа покинет цикл обработки сообщений и завершится с кодом ExitCode. Все не обработанные пользовательской оконной функцией сооб- щения передаются в оконную функцию DefWindowProc(Wnd: HWnd; Msg, wParam: Word; IParam: Longint): Longint; которая задает стандартную реакцию окна на принимаемые сообще- ния. Параметры, передаваемые в DefWindowProc, соответствуют па- раметрам, принимаемым ‘ функцией WindowProc. Значение DefWindowProc возвращается как результат оконной функции поль- зователя. 2.7.11. ПОСЫЛКА СООБЩЕНИЙ Механизм работы с сообщениями предполагает не только прием и обработку, но и посылку сообщений. Источником сообщений может быть не только Windows, но и сама программа или другое приложе- ние. Для генерации сообщений из программы и передачи их соответ- ствующей оконной функции используются функции API: SendMessage, PostMessage, PostQuitMessage.
42 Программирование в среде Borland Pascal для Windows С процедурой PostQuitMessage читатель уже знаком. Строго гово- ря, она является частным случаем более мощной и универсальной функции PostMessage(Wnd: HWnd; Msg, wParam: Word; IParam: Longint): Bool; которая помещает сообщение в очередь прикладной программы и, не дожидаясь его обработки, возвращает управление в точку вызова. Посланное таким образом сообщение будет обработано только после очередного ’обращения к GetMessage или PeekMessage. В случае ус- пешной постановки сообщения в очередь PostMessage возвращает True. Если сообщение должно быть обработано немедленно, использует- ся функция SendMessage(Wnd: HWnd; Msg, wParam: Word; IParam: Longint): Longint; которая вызывает оконную функцию и не возвращает управление до тех пор, пока сообщение не будет обработано. Возвращаемое значе- ние отражает результат обработки и зависит от типа посланного со- общения. Параметры, передаваемые в SendMessage и PostMessage, соответ- ствуют параметрам, принимаемым оконной функцией. 2.7.12. ПЕРВЫЕ ИТОГИ Пора подвести первые итоги. С теорией все ясно и понятно, с практи- кой дело обстоит хуже. Вы видите, что даже простейшая программа реализуется достаточно большим и сложным исходным кодом. Мож- но себе представить, что будет, если потребуется создать не одно, а несколько окон, каждое со своей оконной функцией, когда возрастет число сообщений, на которые программа должна реагировать. На что станут похожи эти оконные функции? Как отладить такую програм- му? Выход один - создать средство, которое позволит уйти с микро- на макро-уровень программирования, что и было сделано в виде биб- лиотеки ObjectWindows.
3 ВВЕДЕНИЕ В OBJECT WINDOWS 3.1. НАЗНАЧЕНИЕ БИБЛИОТЕКИ OBJECT WINDOWS Смеем утверждать, что написать просто и понятно Windows- приложение без использования объектно-ориентированного про- граммирования крайне сложно, в чем читатель уже смог убедиться после изучения “очень простой” программы MINWIN. По этой при- чине приложения для Windows достаточно долго создавала узкая группа программистов-профессионалов высшего класса, что неизбеж- но сужало мировой рынок пользователей Windows. Для упрощения разработки программ фирма* Borland предложила библиотеку OWL (ObjectWindows Library), которая фактически является объектно- ориентированной надстройкой над Windows API. Это мощная и удобная в эксплуатации библиотека настолько упростила процесс написания кода, что теперь каждый желающий может писать про- граммы, по виду ни в чем не уступающие изделиям Microsoft. Программирование на OWL базируется на использовании множе- ства объектов, иерархия которых показана на рис. 3.1. О назначении каждого объекта мы расскажем по ходу изложения материала. 3.2. ПРОСТЕЙШЕЕ OWL-ПРИЛОЖЕНИЕ Изучение OWL начнем с рассмотрения еще одной, действительно про- стой программы, которая не выполняет никакой полезной работы, но служит основой для разработки всех других OWL-приложений: program MinOwl; uses OWindows; type PMinApp = ATMinApp; TMinApp = object(TApplication) procedure InitMainWindow; virtual; end; procedure TMinApp.lnitMainWindow; begin MainWindow := New(PWindow, lnit(nil, ’Hello, ObjectWindows!')); end;
Программирование в среде Borland Pascal для Windows var MinApp: TMinApp; begin MinApp. InitfMinOwl'); MinApp.Run; MinApp. Done; end. Программа MINOWL выводит на экран пустое окно с заголовком ’Hello, ObjectWindows!'. Нетрудно заметить, что результат выполнения примерно тот же, что у программы MINWIN, но исходный текст за- метно короче и читается значительно проще. Несмотря на краткость, программа требует некоторых пояснений. Рис. 3.1. Иерархия объектов OWL
Глава 3. Введение в Object Windows 45 Начнем с начала. Оператор uses подключает к программе модуль OWINDOWS, который содержит основные объекты, используемые при программировании на OWL. Важнейшим из них является TApplication. Он служит объектно-ориентированной основой любой прикладной программы и называется прикладным объектом. TApplication выполняет ряд стандартных действий, которые в обыч- ных Windows-программах приходится программировать самому: • инициализирует первый и все последующие экземпляры прило- жения; • создает главное окно программы и выводит его на экран; • организует цикл приема и обработки сообщений; • упорядоченно завершает приложение с удалением главного окна и всех дочерних окон. Первым шагом создания OWL-программы является объявление наследника объекта TApplication с перекрытием виртуального метода InitMainWindow: PMinApp = ATMinApp; TMinApp = object(TApplication) procedure InitMainWindow; virtual; end; Метод InitMainWindow вызывается автоматически при конструи- ровании объекта TApplication. Его задача - создать объект главного окна программы и присвоить его адрес указателю Main Window (объявлен в TApplication). В нашем примере InitMainWindow создает экземпляр объекта TWindow: procedure TMinApp. InitMainWindow; begin MainWindow := New(PWindow, lnit(nil, 'Hello, ObjectWindows!')); end; В иерархии OWL (см. рис. 3.1) объект TWindow управляет окном. В своих программах вы будете использовать его в качестве базового для своих собственных оконных объектов. Конструктор Init объекта TWindow имеет два параметра: указатель на родительское окно и заголовок окна. Значение nil, передаваемое в первом параметре кон- структора, указывает на то, что создаваемое окно не имеет родитель- ского и является главным окном прикладной программы. Второй параметр, заголовок окна, - это указатель типа PChar на нуль- терминированную строку; если он равен nil, окно создается с пустым заголовком. Основной блок OWL-программы состоит лишь из нескольких опе- раторов и включает конструирование экземпляра TMinApp, запуск его на выполнение и разрушение:
46 Программирование е среде Borland Pascal для Windows var MinApp: TMinApp; begin MinApp.lnit('MinOwr); { инициализация программы} MinApp.Run; {выполнение программы} M inApp. Done; { завершение программы } end. Конструктор Init инициализирует приложение и принимает одно значение - строковый идентификатор (как правило, имя программно- го файла без расширения). Метод Run проверяет, успешно ли прошла инициализация, и запускает цикл обработки сообщений. Деструктор Done завершает приложение после выхода из цикла обработки сооб- щений. Все три метода наследуются объектом TMinApp у TApplication. Область видимости объекта MinApp ограничена только главным операторным блоком. Доступ к его полям и методам из других частей программы осуществляется через указатель Application, объявленный в модуле OWINDOWS. В конструкторе TApplication.Init он инициа- лизируется адресом вызвавшего его экземпляра прикладного объекта (@Self), поэтому из любой точки программы можно добраться до объекта главного окна через ApplicationA.MainWindow. Остановимся на возможностях объекта TApplication более подроб- но и рассмотрим, как в OWL выполняется инициализация приложения и обработка ошибок. 3.3. ИНИЦИАЛИЗАЦИЯ ПРИЛОЖЕНИЯ Как известно, Windows допускает одновременную работу нескольких приложений, каждое из которых может быть запущено еще и в не- скольких экземплярах. Для инициализации экземпляров программы в объекте TApplication объявлены виртуальные методы InitApplication и Initlnstance, вызываемые из конструктора Init. Метод InitApplication вызывается только для первого экземпляра программы (если дескриптор предыдущего экземпляра HPrevInst ра- вен нулю). В объекте TApplication метод InitApplication ничего не делает. В производном объекте он может содержать код глобальной инициализации приложения, выполняемый только один раз незави- симо от числа запущенных копий. Объективно говоря, необходимость в переопределении метода InitApplication возникает крайне редко, поэтому мы не приводим пример с его использованием. Метод Initlnstance инициализирует каждый запущенный экземпляр программы, в том числе и первый. В TApplication он конструирует главное окно (вызывая InitMainWindow) и отображает его на экране. В производном объекте метод Initlnstance может быть расширен до-
Гпава 3. Введение в Object Windows 47 полнительными действиями. Они обычно выполняются до вызова метода предка, когда ни одно окно программы еще не создано. В сле- дующем примере метод Initlnstance перекрыт, чтобы запретить по- вторный запуск программы: program Single; uses OWindows; const em_Sparelnstance = -6; { код ошибки лишнего экземпляра } type PSingleApp = ATSingleApp; TSingleApp = object(TApplication) procedure Initlnstance; virtual; procedure InitMainWindow; virtual; end; { Инициализирует только первый экземпляр приложения } procedure TSingleApp. Initlnstance; begin if HPrevInst = 0 then inherited Initlnstance else Status := em_Sparelnstance; end; { Создает объект главного окна программы } procedure TSingleApp. InitMainWindow; begin MainWindow := New(PWindow, lnit(nll, 'Single Instance')); end; var SingleApp: TSingleApp; begin SingleApp. Init('Single'); SingleApp.Run; SingleApp. Done; end. Если вы попытаетесь загрузить несколько экземпляров программы SINGLE, то увидите на экране сообщение об ошибке. Такое поведе- ние обеспечивает метод TSingleApp.Initlnstance, который вызывает Initlnstance предка только для первого экземпляра программы. Для второго экземпляра он устанавливает поле состояния Status в значе- ние em_SpareInstance, сигнализируя о том, что запущен лишний эк- земпляр. Поле состояния Status определено в объекте TApplication и при нормальном выполнении программы содержит нуль. Установка поля Status в ненулевое значение перед вызовом метода Run приводит к завершению программы с выдачей сообщения об ошибке. Код ошиб- ки берется из поля Status. Список зарезервированных ошибок приве- ден в табл. 3.1. Для индикации повторного запуска приложения мы расширили его константой em_SpareInstance со значением -6.
48 Программирование в среде Borland Pascal для Windows Табл. 3.1. Список зарезервированных кодов ошибок OWL Константа Значение Причина ошибки em_InvalidWindow -1 Ошибка создания окна em_OutOfMemory -2 Недостаток памяти em_InvalidClient -3 Ошибка создания окна-клиента (гл. 7) em_InvalidChild -4 Ошибка создания дочернего окна em_InvalidMainWindow -5 Ошибка создания главного о#на 3.4. ОБРАБОТКА ОШИБОК Наличие хорошей подсистемы обработки ошибок является обязатель- ным атрибутом любой программы, независимо от того, на каком язы- ке она написана. В OWL-программах обработку ошибок выполняет виртуальный метод Error прикладного объекта: TApplication.Error(ErrorCode: Integer); virtual; где Err or Code - код ошибки. Стандартный обработчик ошибок TApplication.Error выдает на экран окно Application Error с кодом ошибки и сообщением Continue? (Продолжить?). В окне имеются две кнопки: Yes и No. Выбирая кноп- ку Yes, пользователь может попытаться продолжить работу с прило- жением. Кнопка No означает немедленный выход из программы. Согласитесь, что такая обработка ошибок недостаточно информа- тивна. Поэтому в своих программах следует перекрывать метод Error. Покажем, как это делается в реальной программе: { ошибка создания главного окна } { ошибка создания дочернего окна } { ошибка создания окна-клиента } { недостаток памяти } { ошибка создания окна } program ErrMsg; uses WinTypes, OWindows, Strings; const em_Sparelnstance = -6; {код ошибки лишнего экземпляра } ErrorStrings: array [em_Sparelnstance..em_lnvalidWindow] of PChar = ( 'Application is already running', {ошибка лишнего экземпляра} 'Invalid main window', 'Invalid child window', 'Invalid client window', 'Out of memory', 'Invalid window'); type PErrMsgApp = ATErrMsgApp; TErrMsgApp = object(TApplication) procedure Initlnstance; virtual; procedure InitMainWindow; virtual; procedure Error(ErrorCode: Integer); virtual; end;
Глава 3. Введение в Object Windows 49 { Инициализирует только первый экземпляр приложения } procedure TErrMsgApp.lnitlnstance; begin if HPrevInst = 0 then inherited Initlnstance else Status := em_Sparelnstance; end; { Создает объект главного окна программы } procedure TErrMsgApp.lnitMainWindow; begin MainWindow := New(PWindow, lnit(nil, 'Single Instance')); end; { Обрабатывает ошибки прикладного объекта } procedure TErrMsgApp.Error(ErrorCode: Integer); var Buf: array [0..63] of Char; P: PChar; begin P := 'Unknown error'; if (ErrorCode >= em_Sparelnstance) and (ErrorCode <= emJnvalldWIndow) then P := ErrorStrings[ErrorCode]; StrCopy(StrECopy(Buf, P), #13#10'Exlt program?'); if MessageBox(0, Buf, 'Application Error', mbJconStop + mb_YesNo) = idYes then Halt; end; var ErrMsgApp: TErrMsgApp; begin ErrMsgApp.lnit('ErrMsg'); ErrMsgApp.Run; ErrMsgApp. Done; end. Рис. 3.2. Окно Application Error Обработчик ошибок TErrMsgApp.Error отображает на экране ок- но Application Error (рис. 3.2) и помещает в него две строки: описание ошибки и предложение завершить программу. Если пользователь
50 Программирование в среде Borland Pascal для Windows выбирает кнопку Yes, программа немедленно завершается; если выби- рается кнопка No, делается попытка продолжить работу. Описания всех известных ошибок хранятся в массиве нуль- терминированных строк ErrorStrings. Код ошибки является индексом в этом массиве. Если код ошибки ErrorCode, переданный в Error, по- падает в диапазон известных ошибок, то строка сообщения, поме- щаемая в окно Application Error, берется из массива ErrorStrings. Ина- че выдается сообщение о неизвестной ошибке - Unknown error. Для выдачи сообщений применяется функция Windows API: MessageBox(WndParent: HWnd; Txt, Caption: PChar; TextType: Word): Integer; где: WndParent - дескриптор родительского окна для создаваемого окна диалога (если он равен нулю, окно диалога не имеет владельца, в качестве владельца ему назначается вся область экрана - desktop); Txt - текст сообщения; Caption - заголовок окна (если он равен nil, то окно получает заголовок Error); TextType - дополнительные свойства окна сообщений, задаваемые комбинацией флагов из табл. 3.2. Табл. 3.2. Флаги функции MessageBox Флаг Описание Флаги кнопок mb_Ok mb_OkCancel В окно помещается кнопка Ок В окно помещаются кнопки Ок и Cancel mb_AbortRetry!gnore В окно помещаются кнопки Abort, Retry, Ignore mb_Y esN oCancel mb.YesNo mb_RetryCancel mb_DefButtonl mb_DefButton2 mb_DefButton3 Флаги пиктограмм mb_IconHand, mbJconStop mbJconQuestion mb_IconExclamation mb_Icon Asterisk, mb_lconlnformation В окно помещаются кнопки Yes, No и Cancel В окно помещаются кнопки Yes и No В окно помещаются кнопки Retry и Cancel Первая кнопка становится кнопкой по умолчанию Вторая кнопка становится кнопкой по умолчанию Третья кнопка становится кнопкой по умолчанию В окно помещается пиктограмма “стоп” В окно помещается пиктограмма “?” В окно помещается пиктограмма “!” В окно помещается пиктограмма “i” Режимы выполнения mb.Appl Modal Модальное окно сообщений (по умолчанию)
Гпава 3. Введение в Object Windows 51 Окончание табл. 3.2 Флаг Описание mb_SystemModal Модальное окно сообщений, которое приостанавлива- ет работу Windows. Применяется в потенциально ава- рийных ситуациях mb_TaskModal Модальное окно сообщений, которое приостанавлива- ет работу всех остальных окон данной задачи. Исполь- зуется при отсутствии доступного родительского окна (значение параметра WndParent устанавливают в нуль) Функция MessageBox отображает простейшее диалоговое окно, в котором помещается пиктограмма (если она задана), текст сообщения и одна или несколько кнопок. После работы функция возвращает идентификатор кнопки, выбранной пользователем (табл. 3.3). Табл. 3.3. Значения функции MessageBox Константа Описание idOk idCancel idAbort idRetry idlgnore idYes idNo Диалог завершился нажатием кнопки Ok Диалог завершился нажатием кнопки Cancel Диалог завершился нажатием кнопки Abort Диалог завершился нажатием кнопки Retry Диалог завершился нажатием кнопки Ignore Диалог завершился нажатием кнопки Yes Диалог завершился нажатием кнопки No В нашем примере функция MessageBox вызывается с флагами mb_IconStop + mb_YesNo, что обеспечивает отображение в окне пик- тограммы Stop и кнопок Yes и No (см. рис. 3.2). Если пользователь нажимает кнопку Yes, MessageBox возвращает значение idYes и про- грамма аварийно завершается оператором Halt. 3.5. ОКНА В OWL Для работы с окнами в OWL имеется набор объектов, которые назы- ваются интерфейсными или оконными. В OWL они формируют интер- фейсные элементы Windows (окна, блоки диалога, элементы управле- ния в окнах) и управляют ими. Оконные объекты можно сравнить с потоковыми объектами, используемыми при работе с файлами. Как потоковый объект создается для управления файлом, так оконный объект создается для управления интерфейсным элементом. Иерархия основных оконных объектов OWL показана на рис. 3.3.
52 Программирование в среде Borland Pascal для Windows Рис. 3.3. Иерархия основных оконных объектов Объект TWindowsObject является базовым для всех оконных объ- ектов OWL. Это абстрактный тип данных, поэтому создавать его эк- земпляры запрещено. TWindowsObject обеспечивает: • связь объекта с интерфейсным элементом Windows; • поддержку отношения “родитель-ребенок” между оконными объектами; • работу механизма ответа на сообщения. Оконный объект связывается с соответствующим ему интерфейс- ным элементом с помощью объявленного в TWindowsObject дескрип- тора HWindow. Когда вы манипулируете объектом, вызывая его ме- тоды, он обращается к функциям Windows, передавая им этот деск- риптор. При прямом вызове функций Windows API значение HWindow передается в качестве дескриптора окна. Отношение “родитель-ребенок” поддерживается с помощью ука- зателя Parent на родительский объект и списка ChildList дочерних объектов. Адрес родительского объекта (значение для Parent) переда- ется конструктору оконного объекта. Список дочерних объектов формируется автоматически - при конструировании объект сам до- бавляет себя в список дочерних объектов родителя. Реакция на сообщения Windows является фундаментальным свой- ством всех оконных объектов. Ее мы рассмотрим позднее. От TWindowsObject порождены объекты TWindow и TDialog. TWindow ассоциируется с обычным окном, a TDialog - с окном диа- лога. Объект TControl порожден от TWindow и является родоначаль- ником иерархии объектов, связанных с элементами управления (кнопками, блоками списков, полосами скроллинга и т.д.). TMDIWindow используется в многооконных приложениях как объект главного окна, содержащего дочерние окна. В этой главе мы рассмотрим работу с обычными окнами, окна диалога описаны в гл. 5, управляющие элементы - в гл. 6, многоокон- ные приложения - в гл. 7.
Глава 3. Введение в Object Windows 53 3.6. СОЗДАНИЕ И ОТОБРАЖЕНИЕ ОКНА Рассмотрим, как происходит создание и отображение окна в OWL- программе. Ключевым моментом в понимании механизма работы оконных объектов является то, что процедуры создания OWL-объекта и связанного с ним Windows-элемента разнесены во времени. Сначала создается оконный объект. Для этого вызывается конст- руктор соответствующего объектного типа, в задачу которого входит подготовка полей объекта для создания по ним в будущем интерфейс- ного элемента. Например, следующий оператор создает экземпляр объекта TWindow: Window := New(PWindow, I nit(The Parent, 'MyWindow')); Здесь переменная TheParent содержит адрес родительского оконного объекта. При конструировании объекта, связанного с главным окном программы, этот параметр должен быть равен nil. Второй параметр определяет заголовок будущего окна. После вызова конструктора поле HWindow оконного объекта еще остается неопределенным, так как связанный с объектом интерфейс- ный элемент еще не создан. Создание окна и его привязку к объекту выполняет виртуальный метод Create. В объекте TWindow он регист- рирует оконный класс (если это необходимо) и создает окно, вызывая соответствующую API-функцию. Дескриптор готового окна помеща- ется в поле HWindow оконного объекта. Вызов метода Create - это быстрый, но не лучший способ создания окна. Более “безопасный” путь состоит в использовании метода MakeWindow объекта TApplication: Window := ApplicationA.MakeWindow(Window); Метод MakeWindow сам вызывает у переданного в параметре объ- екта метод Create, но дополнительно проверяет корректность созда- ния окна и наличие достаточного объема свободной памяти. Если окно создается успешно, MakeWindow возвращает указатель на свя- занный с окном объект. Если происходит ошибка, например из-за недостатка памяти, MakeWindow уничтожает оконный объект и воз- вращает nil. После создания интерфейсный элемент может оставаться скрытым от пользователя. Чтобы его показать, нужно вызвать у оконного объ- екта метод Show: if Window <> nil then WindowA.Show(sw_ShowNormal); Передаваемый параметр определяет вид окна на экране и может принимать значения, перечисленные в табл. 2.3.
54 Программирование в среде Borland Pascal для Windows В любом Windows-приложении можно выделить окно, которое яв- ляется главным. В OWL его создание автоматизировано, нужно толь- ко сконструировать оконный объект и присвоить его адрес указателю MainWindow. Как вы уже знаете, это делает метод InitMainWindow прикладного объекта. Создание и отображение окна выполняет метод TApplication.Initlnstance, исходный текст которого полезно знать каждому программисту: procedure TApplication. I n it I nstance; begin { Создается OWL-объект } InitMainWindow; { Создается Windows-элемент } MainWindow := MakeWindow(MainWindow); if MainWindow <> nil then { Если окно создано успешно, то оно отображается } MainWindowA.Show(CmdShow) else { Иначе регистрируется ошибка } Status := emJnvalidMainWindow; end; Напомним, что переменная CmdShow, передаваемая в метод Show, объявлена в модуле SYSTEM и задает режим отображения главного окна. Если окно создается успешно, то у связанного с ним объекта авто- матически вызывается виртуальный метод SetupWindow. В момент вызова SetupWindow дескриптор HWindow уже имеет реальное значе- ние и может быть использован при обращении к функциям Windows API. Метод SetupWindow часто переопределяется в наследниках для инициализации Windows-элемента после его создания. В этом случае первым оператором всегда вызывается SetupWindow базового объек- та. В качестве примера рассмотрим программу SETUPWND, в кото- рой главное окно центрируется на экране после создания: program SetupWnd; uses WinTypes, WinProcs, OWindows, Strings; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) procedure SetupWindow; virtual; end; PSetupWndApp = ATSetupWndApp; TSetupWndApp = object(TApplication) procedure InitMainWindow; virtual; end; { Процедура центрирования окна на экране } procedure CenterWindow(Wnd: HWnd);
Глава 3. Введение в Object Windows 55 var R: TRect; X, Y, W, H: Integer; begin GetWindowRect(Wnd, R); W := R.Right - R.Left;- H := R.Bottom - R.Top; X := (GetSystemMetrics(sm_CXScreen) - W) div 2; Y := (GetSystemMetrics(sm_CYScreen) - H) div 2; MoveWindow(Wnd, X, Y, W, H, False); end; { Центрирует окно на экране после создания } procedure TMainWindow.SetupWindow; begin inherited SetupWindow; CenferWindow(HWindow); end; { Создает объект главного окна программы } procedure TSetupWndApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'SetupWindow Demo')); end; var SetupWndApp: TSetupWndApp; begin SetupWndApp. I nit('SetupWnd'); SetupWndApp.Run; SetupWndApp. Done; end. Центрирование окна выполняет процедура CenterWindow. Мы сделали ее универсальной и независимой, так что вы можете помес- тить ее в отдельный модуль и использовать в своих программах для центрирования любых окон. Процедура CenterWindow принимает дескриптор окна и выполняет следующие действия: • получает координаты окна, вызывая процедуру Windows GetWindowRect(Wnd: HWnd; var Rect: TRect); где Wnd - дескриптор окна; Rect - запись типа TRect, в которой воз- вращаются координаты левого верхнего (поля Left и Тор) и правого нижнего (поля Right и Bottom) углов окна; • вычисляет ширину (W) и высоту (Н) окна; • рассчитывает новые координаты (X, Y) для центрированного ок- на, исходя из его размеров и размеров экрана. Для определения раз- меров экрана используется функция GetSystemMetrics(Index: Integer): Integer; которая возвращает значение системной метрики, заданной в пара- метре Index (см. прил. Б). Для получения горизонтального и верти-
56 Программирование в среде Borland Pascal для Windows кального размеров экрана GetSystemMetrics вызывается со значения- ми sm.CXScreen и sm_CYScreen соответственно; • перемещает окно в новую позицию с помощью процедуры MoveWindow(Wnd: HWnd; X, Y, Width, Height: Integer; Repaint: Bool); где Wnd - дескриптор окна; X, Y - новые координаты левого верхнего угла окна; Width, Height - новые ширина и высота окна; Repaint - определяет, надо ли перерисовать окно. Метод SetupWindow оконного объекта предельно прост: он вызы- вает метод предка, а затем центрирует окно с помощью процедуры CenterWindow, передавая ей дескриптор HWindow. Отметим, что вы- зов CenterWindow нельзя перенести в конструктор объекта, так как в конструкторе значение дескриптора HWindow еще неизвестно. Это основная причина, по которой некоторые действия приходится вы- полнять не в конструкторе оконного объекта, а в методе SetupWindow. 3.7. АТРИБУТЫ ОКНА Во всех рассмотренных до сих пор OWL-программах мы создавали главное окно, не указывая явно многих его атрибутов, таких, напри- мер, как размер и стиль. Атрибуты окна хранятся в записи Attr объек- та TWindow и получают начальные значения в конструкторе Init. Рассмотрим структуру записи Attr: TWindowAttr = record Title: PChar; Style: Longint; ExStyle: Longint; X, Y, W, H: Integer; Param: Pointer; case Integer of 0: (Menu: HMenu); {дескриптор меню} 1: (Id: Integer); {идентификатор дочернего окна } end; { заголовок окна } { стиль окна } { расширенный стиль окна } { координаты и размеры окна } { указатель на данные пользователя } Поля записи имеют следующее назначение: • Title - указатель на нуль-терминированную строку, содержащую заголовок окна. Значение для этого атрибута передается конструкто- ру TWindow.Init в параметре ATitle; • Style - стили (флаги) окна (табл. 3.5); • ExStyle - дополнительные стили окна (табл. 3.6); • X, Y - начальные координаты левого верхнего угла окна. Для окон со стилем ws_Overlapped или ws_Popup они отсчитываются от левого верхнего угла экрана. Для окон со стилем ws_Child координа- ты отсчитываются от левого верхнего угла рабочей области родитель-
Гпава 3. Введение в Object Windows 57 ского окна. Конструктор Init объекта TWindow устанавливает поле X в значение cwJUseDefault, а поле Y - в нуль. Если значение X- координаты при создании окна равно cw.UseDefault, то Windows игнорирует значение параметра Y и выбирает местоположение левого верхнего угла окна самостоятельно. Это выполняется только для окон без родителя со стилем ws_Overlapped; • W, Н - начальные ширина и высота окна. Конструктор Init объ- екта TWindow устанавливает W в значения cw_UseDefault, а Н - в нуль. Если при создании окна его ширина равна cw_UseDefault, Windows игнорирует значение высоты окна и выбирает размеры окна самостоятельно. Это выполняется только для окон без родителя со стилем ws_Overlapped. Значения параметров X, Y, W и Н указываются в пикселах; © Param - указатель на любые данные пользователя (фактически это резерв на будущее). © Menu, Id - взаимоисключающие поля. Если окно создается со стилем ws_Overlapped или ws_Popup, в поле Menu заносится дескрип- тор связанного с окном меню. Если он равен нулю, то в окне устанав- ливается меню оконного класса. Для окон со стилем ws_Child исполь- зуется поле Id, в него помещается уникальный идентификатор дочер- него окна в родительском. Назначение элементов данных Menu и Id прояснится при изучении ресурсов Windows в гл. 5. Табл. 3.5. Основные стили окна Константа Описание ws_Overlapped, ws_Tiled Окна с этим стилем называют перекрывающимися. Стиль ws_Overlapped принимается по умолчанию, если не установлен стиль ws_Popup или ws.Child. (Заметим, что окна типа ws.Popup и ws.Child тоже могут перекрываться.) Окно типа ws.Overlapped всегда имеет заголовок и границу. Если при создании такого окна вместо координат или размеров исполь- зуется константа cw.UseDefault, то Windows сама устанавливает наиболее подходящие с ее точки зрения значения для этих атрибутов. Стиль ws.Overlapped назначается только главному окну программы ws.Popup Всплывающее (вспомогательное) окно. Этот тип окон чаще всего используется для отображения диалого- вых блоков. Если всплывающее окно имеет родителя, то отображается над другими окнами своего прило- жения. Стиль ws.Popup не может использоваться совместно со стилем ws.Child ws.Child Дочернее окно. Окна этого типа всегда имеют родительское окно, отображаются внутри него и не
58 Программирование в среде Borland Pascal для Windows Продолжение табл. 3.5 Константа Описание ws_MaximizeBox, ws_TabStop ws_MinimizeBox, ws_Group ws_ThickFrame, ws_SizeBox ws_SysMenu ws_H Scroll ws_VScroll ws_DlgFrame ws_Border видны за его пределами. Координаты дочерних окон отсчитываются не от левого верхнего угла экрана, а от левого верхнего угла рабочей области окна- родителя. При перемещении родительского окна дочерние окна перемещаются вместе с ним, так что их относительные координаты остаются неизменными. Дочерние окна применяются для организации эле- ментов управления в блоках диалога, а также в про- граммах с многооконным интерфейсом. Так как до- черними часто называют любые окна, имеющие ро- дителя, может возникнуть неоднозначность в пони- мании термина “дочерний”. Чтобы исключить неод- нозначность, окна со стилем ws_Child будем называть дочерними окнами типа ws_Child. Стиль ws_Child не может использоваться совместно со стилем ws_Popup Окно создается с кнопкой максимизации. Если окно является элементом управления в окне диалога, то этот флаг используется под именем ws_TabStop. Стиль ws_TabStop определяет, будет ли управляю- щий элемент захватывать клавиатурный ввод при выборе его в окне диалога Окно создается с кнопкой минимизации. Если окно является управляющим элементом окна диалога, то этот флаг используется под именем ws_Group. Эле- мент управления с установленным стилем ws_Group определяет начало группы элементов управления, в пределах которой можно перемещаться с помощью клавиш управления курсором. Концом одной группы является начало другой Окно создается с рамкой, предназначенной для изме- нения размеров окна. Изменять размеры можно толь- ко у того окна, в котором этот стиль установлен Окно создается с системным меню. Стиль действует только в окне с заголовком (со стилем ws_Caption) Окно с горизонтальной полосой просмотра Окно с вертикальной полосой просмотра Окно без заголовка с широкой рамкой, которую обычно имеют модальные окна диалога. Стиль wsJDIgFrame не может использоваться со стилем ws_Caption. Для формирования диалогового окна с широкой рамкой и заголовком нужно вместо стиля wsJDIgFrame указать ws_Caption и ds_ModalFrame Окно с тонкой черной границей в один пиксел
Глава 3. Введение в Object Windows 59 Окончание табл. 3.5 Константа Описание ws_Caption Окно с заголовком и тонкой черной границей толщи- ной в один пиксел. Такое окно можно перемещать по экрану с помощью мыши. Стиль ws_Caption не может использоваться со стилем ws_DlgFrame ws_Maximize ws_ClipChildren Окно создается с максимальным размером При перерисовке родительского окна из его рабочей области исключается область, занимаемая дочерними окнами. Стиль ws_ClipChildren используется только в родительских окнах ws_ClipSiblings При перерисовке дочернего окна из его рабочей об- ласти исключается область, занимаемая другими дочерними окнами родительского окна. Если дочер- ние окна перекрываются, а флаг ws_ClipSiblings не указан, то при изменении рабочей области одного из окон могут быть испорчены рабочие области других дочерних окон. Стиль ws_ClipSiblings используется только в дочерних окнах типа ws_Child ws_Disabled ws_Visible ws_Minimize, ws_Iconic Окно становится недоступным Окно отображается сразу после создания Окно создается свернутым в пиктограмму ws_pverlappedWindow, Сложный стиль, являющийся комбинацией других ws_TiledWindow стилей - ws_Overlapped, ws_Caption, ws_SysMenu, ws_ThickFrame, ws_MinimizeBox, ws_MaximizeBox ws_PopupWindow Сложный стиль, являющийся комбинацией других стилей - ws_Popup, ws_Border, ws_SysMenu. Для того чтобы системное меню было видно на экране, к ws_PopupWindow нужно добавить стиль ws_Caption ws_iChildWindow Аналог ws_Child Табл. 3.6. Дополнительные стили окна Константа Описание ws_ex_DlgModalFrame Окно с широкой рамкой, свойственной модальным ws_ex_NoParentNotify блокам диалога. Окно с этим стилем может иметь еще и заголовок, если указан флаг ws_Caption Указывает, что дочернее окно типа ws_Child не будет уведомлять родительское окно о своем создании и уничтожении ws_ex_T opMost Окно, которое всегда (даже когда оно неактивно) располагается над всеми остальными окнами
60 Программирование в среде Borland Pascal для Windows Окончание табл. 3.6 Константа Описание ws_eX_AcceptFiles ws_ex_T ransparent Окно поддерживает технологию “перетяни-и-оставь” Прозрачное окно. Оно не закрывает находящиеся под ним окна и перерисовывается после всех родственных с ним (по горизонтали) окон Попробуем теперь создать оконный объект, в котором изменим значения некоторых его атрибутов. Для этого объявим наследника TWindow (объект TMainWindow) и перекроем в нем конструктор Init: constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); with Attr do begin ExStyle := ExStyle or ws_ex_TopMost; X := 80; Y := 80; W := 320; H := 160; end; end; В TMainWindow.Init сначала вызывается конструктор базового объекта, который присваивает полям записи Attr начальные значения. Затем модифицируются дополнительный стиль, координаты и разме- ры окна. Стиль ws_ex_TopMost появился в Windows версии 3.1, по- этому к программе подключается модуль WIN31: program WndAttr; uses WinTypes {$ifndefVer80}, Win31 {Sendif}, OWindows; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) constructor lnit(AParent: PWindowsObject; ATitle: PChar); end; PWndAttrApp = ATWndAttrApp; TWndAttrApp = object(TApplication) procedure InitMainWindow; virtual; end; { Конструирует оконный объект и инициализирует его поля } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); with Attr do begin ExStyle := ExStyle or ws_ex_TopMost; X := 80; Y := 80; W := 320; H := 160; end; end;
Гпава 3. Введение в bbject Windows 61 { Создает объект главного окна программы } procedure TWndAttrApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'Top Most Window')); end; var WndAttrApp: TWndAttrApp; begin WndAttrApp. Init(WndAttr'); WndAttrApp.Run; WndAttrApp. Done; end. Как изменение атрибутов окна отразилось на поведении програм мы WNDATTR? Во-первых, все экземпляры программы WNDATTR при запуске отображают окна одинаковых размеров в одном и том же месте экрана. Во-вторых, окно программы WNDATTR всегда, даже когда оно неактивно или минимизировано, располагается над осталь- ными окнами (результат действия флага ws_ex_TopMost). 3.8. ОБРАБОТКА СООБЩЕНИЙ В OWL 3.8.1. ОСНОВНАЯ ИДЕЯ В примере MINWIN было показано, как программа, написанная без OWL, принимает и обрабатывает сообщения Windows. Для обработки сообщений, адресованных окну, приложение сообщает Windows адрес функции обработки сообщений, которую называют оконной функци- ей. Ее адрес хранится в структуре оконного класса. Каждый оконный класс имеет свою собственную оконную функцию, которая обрабаты- вает сообщения от всех окон, принадлежащих данному классу. Процедурное программирование предполагает, что получателем сообщения является некоторая функция прикладной программы. Внутри этой функции обработка сообщений осуществляется с помо- щью оператора case. Он выбирает в соответствии с кодом поступив- шего сообщения нужный операторный блок, в котором определена реакция на это сообщение. При таком стиле программирования одна функция обработки сообщений обычно занимает несколько страниц даже в небольших по объему программах. Однако основное неудобст- во состоит в том, что функция обработки сообщений отделена от дан- ных, с которыми она работает. Все это усложняет не только програм- мирование, но и отладку. Библиотека OWL с ее механизмом обработки сообщений предла- гает удобное и элегантное решение этой проблемы, идея которого состоит в следующем. Если для управления окном в OWL создается оконный объект, то логично, чтобы получателем сообщений, адре-
62 Программирование в среде Borland Pascal для Windows суемых окну, был этот же объект. Функция обработки сообщений в OWL переадресует (диспетчирует) вызов к нужному методу оконного объекта. Эти методы называются методами ответа на сообщения. Каждый такой метод реагирует на сообщения только одного типа. Вместо того, чтобы программировать оконную функцию, OWL пред- лагает объявить наследника одного из своих оконных объектов (в зависимости от того, какое окно и с какими свойствами вы хотите получить) и определить методы ответа на сообщения, поступающие в этот объект. Так как методы ответа на сообщения являются виртуаль- ными, в производном объекте их можно перекрывать, с тем чтобы они работали иначе. 3.8.2. ДИНАМИЧЕСКИЕ ВИРТУАЛЬНЫЕ МЕТОДЫ Для реализации описанной выше схемы обработки сообщений объек- ты языка BPW были расширены динамическими виртуальными мето- дами (далее слово виртуальный мы будем опускать, употребляя тер- мин динамический метод). В отличие от обычного виртуального мето- да объявление динамического метода включает определение так на- зываемого виртуального индекса (индекса диспетчирования)'. TBase = object procedure DynamicMethod(X, Y: Integer); virtual 100; end; Виртуальный индекс записывается после слова virtual и должен быть константным выражением типа Word, значение которого боль- ше нуля. Он используется для вызова динамического метода. Для каждого объекта, содержащего динамические методы, компи- лятор строит специальную таблицу, называемую таблицей динамиче- ских методов - DMT (от англ. Dynamic Method Table). Она хранит виртуальные индексы и соответствующие им адреса динамических методов. Вызов динамического метода осуществляется через специальную процедуру диспетчер, которая по заданному виртуальному индексу отыскивает в таблице DMT его адрес. Если в DMT данного объекта метод с указанным виртуальным индексом не найден, то диспетчер повторяет поиск в DMT базового объекта. Поиск продолжается до тех пор, пока не найден метод с заданным индексом или не достигнута вершина объектной иерархии. Если динамический метод найден, то диспетчер передает ему управление, иначе вызывается процедура об- работки по умолчанию. Формат объявления нового динамического метода (число и тип принимаемых параметров, процедура или функция, тип возвращаемо- го значения) может быть произвольным. Однако в производных объ- ектах изменение виртуального индекса, количества или типа парамет-
Глава 3. Введение в Object Windows 63 ров, типа возвращаемого значения (для функций), а также определе- ние другого динамического метода с таким же виртуальным индек- сом, является ошибкой. Динамический метод нельзя перекрывать ста- тическим или обычным виртуальным, и наоборот. Приведем пример переопределения динамического метода DynamicMethod (см. выше) в производном объекте TDerived: TDerived = object(TBase) procedure DynamicMethod(X, Y: Integer); virtual 100; end; Хотя динамические методы вызываются дольше виртуальных (из- за задержки, связанной с поиском по таблицам DMT), они обеспечи- вают большую гибкость при программировании, поскольку не нуж- даются в предварительном объявлении в базовом объекте. 3.8.3. МЕТОДЫ ОТВЕТА НА СООБЩЕНИЯ В OWL динамические методы используются для ответа на сообщения Windows и оформляются следующим образом: TMyWindow = object(TWindow) procedure WML Button Down( var Msg: TMessage); virtual wm_First + wm_L Button Down; end; По соглашению имя метода ответа на сообщение получается уда- лением из идентификатора сообщения символа подчеркивания и пре- образованием префикса (wm_ и т.п.) из строчных букв в прописные. Например, метод ответа на сообщение wmJLButtonDown имеет имя WMLButtonDown. Такое соглашение улучшает читабельность про- грамм, но не является строгим, поскольку для работы метода важен его виртуальный индекс, а не имя. Виртуальные индексы методов ответа на сообщения состоят из суммы двух компонентов: селектора диапазона и номера сообщения Windows в этом диапазоне. Этому правилу должны подчиняться все методы ответа на сообщения Windows. Существующие диапазоны сообщений приведены в табл. 3.7. Например, селектор wm_First открывает диапазон всех сообщений Windows, большинство из которых имеет префикс wm_. Селектор cm_First начинает диапазон сообщений от пунктов меню и акселера- торных клавиш (см. гл. 6). Селектор id_First начинает диапазон сооб- щений от управляющих элементов. Метод ответа на сообщение имеет один параметр - запись типа TMessage с информацией о поступившем сообщении: TMessage = record Receiver: HWnd;
64 Программирование в среде Borland Pascal для Windows Message: Word; case Integer of 0:( WParam: Word; LParam: Longint; Result: Longint); 1:( WParamLo: Byte; WParamHi: Byte; LParamLo: Word; LParamHi: Word; ResultLo: Word; ResultHi: Word); end; Поля записи совпадают по смыслу с параметрами, которые полу- чает оконная функция (см. 2.7): Receiver - дескриптор окна- получателя; Message - код сообщения; WParam, LParam - специфиче- ская для конкретного сообщения информация. Дополнительное поле Result заполняется методом ответа на сообщение, его значение воз- вращается в Windows как результат обработки. Табл. 3.7. Диапазоны сообщений Константа Значение Диапазон Описание wm_First $0000 $0000 - $7FFF Сообщения общего назначения id_First $8000 $8000 - $8FFF Сообщения дочерних окон, прини- маемые в родительском объекте id-Intemal $8F00 $8F00 - $8FFF Зарезервированный поддиапазон nf_First $9000 $A000 - $FFFF Сообщения управляющих элементов, принимаемые в их оконных объектах nf_Internal $9F00 $9F00 - $9FFF Зарезервированный поддиапазон cm_First $A000 $A000 - $FFFF Командные сообщения от пунктов меню, акселераторных клавиш и кно- пок инструментальной панели (гл. 6) cm_Internal $FF00 $FF00-$FFFF Зарезервированный поддиапазон Рассмотрим теперь, как в OWL-программе обрабатываются сооб- щения от мыши, клавиатуры и таймера. 3.9. СООБЩЕНИЯ ОТ МЫШИ При каждом нажатии и отпускании левой кнопки мыши Windows посылает программе сообщения wm_LButtonDown и wm_LButtonUp, а при нажатии и отпускании правой кнопки мыши, - сообщения
Гпава 3. Введение в Object Windows 65 wm_RButtonDown и wm_RButtonUp. При перемещении мыши (с на- жатой или отпущенной кнопкой) Windows периодически генерирует и помещает в очередь приложения сообщения wm_MouseMove. Если в сеансе работы задействована трехкнопочная мышь, то программа может дополнительно получать сообщения wm_MButtonDown и wm_MButtonUp, генерируемые средней кнопкой. Несмотря на такую возможность, большинство Windows-программ не полагаются на наличие средней кнопки и обрабатывают сообщения только от левой и правой кнопок. Если в стиле оконного класса установлен флаг csJDblClks, то окно способно получать еще сообщения двухкратного щелчка: wm_LButtonDblClk - для левой кнопки, wm_RButtonDblClk - для правой кнопки и wm_MButtonDblClk - для средней. Когда двухкрат- ные щелчки разрешены, Windows устанавливает один из своих внут- ренних таймеров на интервал, заданный при настройке мыши в Control Panel. Если пользователь нажимает, отпускает, а затем снова нажимает кнопку мыши в пределах этого интервала времени, то Windows вместо повторного нажатия посылает сообщение двухкрат- ного щелчка. Таким образом, в результате двухкратного нажатия, скажем левой кнопки мыши, приложение получит подряд четыре со- общения: wm_LButtonDown, wm_LButtonUp, wm_LButtonDblClk и затем wm_LButtonUp. В сообщении от мыши (запись Msg) передаются следующие пара- метры: • WParam - состояние кнопок мыши и клавиш Shift и Ctrl. Значе- ние кодируется следующими флагами: mk_LButton - нажата левая кнопка; mk_RButton - нажата правая кнопка; mk_MButton - нажата средняя кнопка; mk_Shift - нажата клавиша Shift; mk_Control - нажа- та клавиша Ctrl; • LParamLo - координата X курсора мыши; • LParamHi - координата Y курсора мыши. Наша следующая программа MOUSEMSG реагирует на все основ- ные сообщения двухкнопочной мыши. При обычном нажатии и от- пускании кнопок программа издает звуковой сигнал. При двухкрат- ном щелчке на экран выводится сообщение Left Button Double Click, если была нажата левая кнопка, и Right Button Double Click, - если правая. Перемещение мыши в пределах рабочей области окна сопро- вождается изменением его заголовка, в котором отображаются коор- динаты курсора мыши. Приведем текст программы: program MouseMsg; uses WinTypes, WinProcs {$ifdefVer80}, Messages {$endif}, OWindows; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) 3 Зак. 1049
66 Программирование в среде Borland Pascal для Windows function GetClassName: PChar; virtual; procedure GetWindowClass(var AWndClass: TWndClass); virtual; procedure WMLButtonDown(var Msg: TMessage); virtual wm_First + wm_LButtonDown; procedure WMRButtonDown(var Msg: TMessage); virtual wm_First + wm_RButtonDown; procedure WMLButtonUp(var Msg: TMessage); virtual wm_First + wm_LButtonllp; procedure WMRButtonUp(var Msg: TMessage); virtual wm_First + wm_RButtonllp; procedure WMMouseMove(var Msg: TMessage); virtual wmFirst + wmMouseMove; procedure WMLButtonDblClk(var Msg: TMessage); virtual wm_First + wm_LButtonDblClk; procedure WMRButtonDblClk(var Msg: TMessage); virtual wm_First + wm_RButtonDblClk; end; PMouseMsgApp = ATMouseMsgApp; TMouseMsgApp = object(TApplication) procedure InitMainWindow; virtual; end; { Возвращает имя нового оконного класса } function TMainWindow.GetClassName: PChar; begin GetClassName := 'MouseWindow'; end; { Возвращает атрибуты нового оконного класса } procedure TMainWindow.GetWindowClass(var AWndClass: TWndClass); begin inherited GetWindowClass(AWndClass); with AWndClass do Style := Style or cs_DblClks; end; { Выдает звуковой сигнал при нажатии левой кнопки мыши } procedure TMainWindow.WMLButtonDown(var Msg: TMessage); begin MessageBeep(mb_Ok); end; { Выдает звуковой сигнал при нажатии правой кнопки мыши } procedure TMainWindow.WMRButtonDown(var Msg: TMessage); begin MessageBeep(mb_Ok); end; { Выдает звуковой сигнал при отпускании левой кнопки мыши } procedure TMainWindow.WMLButtonllp(var Msg: TMessage); begin MessageBeep(mb_Ok); end; { Выдает звуковой сигнал при отпускании правой кнопки мыши } procedure TMainWindow.WMRButtonllp(var Msg: TMessage); begin
Гпава 3. Введение в Object Windows 67 MessageBeep(mb_Ok); end; { При перемещении мыши отображает в заголовке окна ее координаты } procedure TMainWindow.WMMouseMove(var Msg: TMessage); var Caption: array [0..47] of Char; begin WVSPrintF(Caption, 'Mouse Position - (%i, %i)', Msg.LParam); SetCaption(Caption); end; { Выдает на экран сообщение при двухкратном щелчке левой кнопки мыши } procedure TMainWindow.WMLButtonDblClk(var Msg: TMessage); begin MessageBox(HWindow, 'Left Button Double Click', 'Message', mb_Ok); end; { Выдает на экран сообщение при двухкратном щелчке правой кнопки мыши } procedure TMainWindow.WMRButtonDblClk(var Msg: TMessage); begin MessageBox(HWindow, 'Right Button Double Click', 'Message', mb_Ok); end; { Создает объект главного окна программы } procedure TMouseMsgApp. InitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'Mouse Messages*)); end; var MouseMsgApp: TMouseMsgApp; begin MouseMsgApp.lnit('MouseMsg'); MouseMsgApp.Run; MouseMsgApp.Done; end. Чтобы установить в оконном классе стиль cs_DblClks и таким об- разом разрешить сообщения двухкратного щелчка, в объекте TMainWindow понадобилось перекрыть виртуальный метод GetWindowClass. Этот метод возвращает запись типа TWndClass, которая содержит атрибуты регистрируемого оконного класса. Чтобы не задавать всех полей записи, в GetWindowClass сначала вызывается перекрытый метод предка, а затем изменяется только стиль класса: procedure TMainWindow.GetWindowClass(var AWndClass: TWndClass); begin inherited GetWindowClass(AWndClass); with AWndClass do Style := Style or csJDbICIks; end; Вместе с методом GetWindowClass всегда перекрывается еще один виртуальный метод - GetClassName, который возвращает имя окон- ного класса:
68 Программирование в среде Borland Pascal для Windows function TMainWindow.GetClassName: PChar; begin GetClassName := 'MouseWindow'; end; По умолчанию наследники TWindow регистрируют оконный класс с именем ’TurboWindow’. Изменяя параметры оконного класса, мы обязаны назначить ему уникальное имя. Если этого не сделать, то одновременно запущенные Windows-программы, написанные на OWL, могут работать неправильно, так как оконные классы всех при- ложений, кроме первого, зарегистрированы не будут. В ответ на нажатие и отпускание кнопок мыши методы WMLButtonDown, WMRButtonDown, WMLButtonUp и WMRButtonUp вызывают функцию MessageBeep(BeepType: Word); которая издает звуковой сигнал заданной тональности. Эта функция очень удобна при отладке, так как может обеспечить звуковое сопро- вождение событий, обычно невидимых при выполнении программы. Например, когда вы не уверены, вызывается некоторый метод ответа на сообщение или нет, поместите в него оператор MessageBeep(O). Метод WMMouseMove вызывается при перемещении мыши и вы- водит в заголовок окна ее координаты: procedure TMainWindow.WMMouseMove(var Msg: TMessage); var Caption: array [0..47] of Char; begin WVSPrintF(Caption, 'Mouse Position - (%i, %i)', Msg.LParam); SetCaption(Caption); end; Новый заголовок, содержащий последние координаты мыши, сна- чала формируется в локальной переменной Caption с помощью функ- ции WVSPrintF, а затем помещается в окно вызовом метода SetCaption. Функция WVSPrintF выполняет форматированный вывод в строковый буфер, ее описание можно найти в справочнике по Borland Pascal. В нашем примере строка формата содержит два спе- цификатора %i, вместо которых в выходной буфер помещаются коор- динаты X и Y указателя мыши (соответственно младшее и старшее слова Msg.LParam). 3.10. СООБЩЕНИЯ ОТ КЛАВИАТУРЫ Обработка клавиатурного ввода осуществляется почти так же, как и обработка сообщений мыши: в оконном объекте объявляется метод
Гпава 3. Введение в Object Windows 69 реакции на одно из сообщений* клавиатуры, которые Windows посы- лает в ответ на нажатия клавиш пользователем, например: TMyWindow = object(TWindow) procedure WMKeyDown(var Msg: TMessage); virtual wm_First + wm_KeyDown; end; Windows позволяет программе, с одной стороны, реагировать на собственно нажатия и отпускания клавиш, а с другой - использовать клавиатуру как устройство ввода символов. О нажатии и отпускании клавиш на клавиатуре программа извещается с помощью сообщений wm_KeyDown (wm_SysKeyDown) и wm_KeyUp (wm_SysKeyUp). Сообщение wm_KeyDown соответствует нажатию, a wm_KeyUp - отпусканию клавиши. Сообщения wm_SysKeyDown и wm_SysKeyUp соответствуют системному нажатию и отпусканию. Системные нажа- тия широко используются для управления программами. В клавиатуре IBM PC системной клавишей считается Alt, а системными нажатиями - нажатия клавиш в комбинации с Alt. Сообщение wm_KeyDown обычно поступает в паре с wm_KeyUp, однако если пользователь нажал на клавишу и удерживает ее некото- рое время, происходит автоповтор нажатой клавиши. При этом Windows начинает автоматически генерировать и помещать в очередь приложения сообщения wm_KeyDown. В результате перед сообщени- ем wm_KeyUp приложение может получить несколько сообщений wm_KeyDown. Если нажатой клавише соответствует отображаемый символ, то, кроме сообщения о нажатии, Windows посылает программе сообще- ние о приходе символа. К числу символьных сообщений относятся wm_Char, wm_DeadChar, wm_SysChar и wm_SysDeadChar. Символь- ные сообщения возникают в результате трансляции клавиатурных со- общений функцией TranslateMessage, которая вызывается в цикле обработки сообщений. Сообщения wm_Char и wm_DeadChar образу- ются из сообщения wm_KeyDown, а сообщения wm_SysChar и wm_SysDeadChar - из сообщения wm_SysKeyDown. Основное назна- чение символьных сообщений - ввод текста. Рассмотрим параметры клавиатурных и символьных сообщений: • WParam - для клавиатурных сообщений содержит виртуальный код клавиши, а для символьных - код символа. Виртуальный код ис- пользуется для идентификации клавиши и относится к некоторой универсальной клавиатуре персонального компьютера. Виртуальные коды клавиш и соответствующие им клавиши на клавиатуре IBM PC приведены в прил. В; • LParam - для клавиатурных и символьных сообщений имеет оди- наковый смысл и содержит счетчик повторов, скан-код, индикатор
70 Программирование в среде Borland Pascal для Windows расширенной клавиши, индикатор системной клавиши, индикатор предыдущего состояния и индикатор текущего состояния (рис. 3.4). 31 30 29 28-27 26-25 24 23-16 15-00 *“ Счетчик повтора (Repeat Count) *- Скан-код L- Индикатор расширенной клавиши (Extended Key) I- Зарезервировано - Используется только Windows I- Индикатор системной клавиши (Context Code) *- Индикатор предыдущего состояния (Previous Key State) - Индикатор текущего состояния (Transition State) Рис. 3.4. Поля параметра LParam в сообщениях клавиатуры Счетчик повтора служит для увеличения фактической емкости очереди сообщений и позволяет избежать потери символов. Если при- ложение не успевает обрабатывать клавиатурные сообщения, то перед тем как поместить сообщение в очередь, Windows объединяет одина- ковые сообщения, увеличивая счетчик повтора. Скан-код или код OEM (от англ. Original Equipment Manufacturer) генерируется аппаратно и зависит от производителя клавиатуры. Индикатор расширенной клавиши равен 1, если была нажата рас- ширенная клавиша, например функциональная клавиша или клавиша на дополнительной цифровой клавиатуре. Индикатор системной клавиши равен 1, если клавиша была нажата в комбинации с клавишей Alt, и 0 в противном случае. Индикатор предыдущего состояния равен 1, если до этого клавиша была нажата, и 0, если отпущена. Индикатор текущего состояния равен 0, если клавиша нажата, и 1, если клавиша отпущена. Нетрудно заметить, что параметры клавиатурных сообщений не несут информации о состоянии клавиш Ctrl и Shift. Чтобы ее полу- чить, нужно вызвать функцию GetKeyState, например: KeyState := GetKeyState(vk_Control); Функция принимает в качестве параметра виртуальный код кла- виши и возвращает ее состояние - “нажата-отпущена”, а для клавиш переключателей (например, Caps Lock) - еще “включена-выключена”. Если функция возвращает отрицательную величину (старший бит равен 1), то клавиша нажата, иначе - отпущена. Для клавиш переклю-
Гпава 3. Введение в Object Windows 71 чателей установленный младший бит свидетельствует о том, что кла- виша находится во включенном состоянии. Функция GetKeyState син- хронизирована с моментом посылки последнего клавиатурного сооб- щения, т.е. она возвращает состояние клавиши не на момент ее вызо- ва, а на момент возникновения последнего сообщения от клавиатуры. Это очень удобно, так как позволяет легко отслеживать нажатия лю- бых комбинаций клавиш. Другая функция опроса клавиатуры - GetAsyncKeyState позволяет определить состояние клавиши именно на момент ее вызова. Предположим, что в программе требуется отследить нажатия кла- виш Ноте и End и комбинаций Ctrl+Home и Ctrl+End. Для этого определяется метод реакции на сообщение wm_KeyDown: procedure TMyWindow.WMKeyDown(var Msg: TMessage); var CtrlPressed: Boolean; begin { Определяем состояние клавиши Ctrl} CtrlPressed := GetKeyState(vk_Control) < 0; case Msg.WParam of vk_Home: if CtrlPressed then {Действия при нажатии клавиш Ctrl+Home } else {Действия при нажатии клавиши Ноте } vk_End: if CtrlPressed then {Действия при нажатии клавиш Ctrl+End} else {Действия при нажатии клавиши End} end; end; В этой процедуре информацию о состоянии клавиши Ctrl на момент нажатия клавиш Ноте и End возвращает функция GetKeyState. Вообще говоря, обрабатывать клавиатурные сообщения в про- грамме приходится крайне редко, ведь Windows всячески старается избавить от этого программиста. Например, если вы работаете со строкой ввода, то вся логика редактирования строки реализуется Windows, а программа получает лишь результат - введенную строку. Не существует проблем и с многострочным редактором, который в Windows тоже имеется. Клавиатурные сообщения не стоит обрабаты- вать даже тогда, когда в программе требуется обрабатывать резуль- таты нажатий “горячих” или, в терминологии Windows, акселератор- ных клавиш. Для этого существуют таблицы акселераторов (см. 5.5), которые преобразуют сообщения о нажатии клавиш и их комбинаций в более осмысленные сообщения - команды. В тех случаях, когда про-
72 Программирование в среде Borland Pascal для Windows грамма все-таки реагирует на клавиатурные сообщения (например, при скроллинге изображения в окне с помощью клавиш перемещения курсора и смены страниц), используется лишь сообщение \vm_KeyDown. 3.11. СООБЩЕНИЯ ОТ ТАЙМЕРА Таймер - это системный генератор событий, который периодически сообщает программе о завершении заданного промежутка времени. Интервал времени между таймерными событиями может уста- навливаться в диапазоне от I до 65535 миллисекунд. Однако прило- жения не могут получать таймерные события чаще, чем 18.2 раза в секунду, и установленный интервал округляется. Так как Windows является псевдомногозадачной средой, интервалы между таймерными событиями оказываются неточными. Поэтому получение таймерного события - это лишь сигнал о том, что заданный промежуток времени истек. Существует два способа перехвата событий от таймера. Первый предполагает перехват сообщения wm_Timer в окне программы. Вто- рой способ состоит в регистрации callback-функции специального формата. Метод приема сообщений wm_Timer проще и по точности не уступает callback-функции; его мы и будем использовать. Второй способ удобен, когда таймерные события должна получать сама при- кладная программа, а не одно из ее окон. Рассмотрим программу TIMERMSG, которая спустя несколько се- кунд после потери активности (при переключении на другую задачу) сама минимизирует свое главное окно. Если пользователь успевает переключиться на нее повторно до истечения заданного промежутка времени, то минимизация не выполняется. Реализация такой программы требует определения в главном оконном объекте методов ответа на сообщения wm.ActivateApp и wm_Tiiner: TMainWindow = object(TWindow) procedure WMActivateApp(var Msg: TMessage); virtual wm_First + wm_ActivateApp; procedure WMTimer(var Msg: TMessage); virtual wm_First + wm_Timer; end; Сообщение wm_ActivateApp посылается всем окнам верхнего уровня, когда происходит переключение между данным приложением и остальными задачами. Если при переключении приложение получа- ет активность, то параметр WParam записи TMessage не равен нулю, если теряет активность - WParam принимает значение нуль. При по-
Гпава 3. Введение в Object Windows 73 тере активности окно устанавливает таймер на 5 секунд, вызывая функцию SetTimer: procedure TMainWindow.WMActivateApp(var Msg: TMessage); begin if Msg.WParam = 0 then SetTimer(HWindow, Timerld, 5000, nil) else KillTimer(HWindow, Timerld); DefWndProc(Msg); end; Чтобы таймерные сообщения приходили в окно программы, пер- вым параметром в вызове SetTimer передается его дескриптор HWindow. Второй параметр - это некоторая целочисленная константа Timerld - идентификатор таймера в окне (в примере Timerld = 1). Идентификатор таймера передается в параметре WParam сообщения wm_Timer, что позволяет иметь несколько таймеров для одного окна. Третий параметр определяет интервал времени в миллисекундах. Величина 5000 означает приблизительно 5 секунд. Последний пара- метр - адрес callback-функции для приема событий от таймера. Значе- ние nil указывает, что таймерные события нужно посылать окну как сообщения wm_Timer. Если пользователь успевает переключиться на приложение до ис- течения заданного промежутка времени, таймер уничтожается вызо- вом функции KillTimer, чтобы при работе вдруг не произошла мини- мизация окна. В KillTimer передается дескриптор окна (HWindow) и идентификатор таймера в этом окне (Timerld). Когда программа по- лучает активность первый раз (при запуске), главное окно пытается удалить таймер, которого еще нет. Мы не считаем это ошибкой, так как при попытке удалить несуществующий таймер KillTimer просто ничего не делает. Если окно получает сообщение wm_Timer, то это значит, что в те- чение 5 секунд приложение оставалось пассивным, и окно должно быть минимизировано: procedure TMainWindow.WMTimer(var Msg: TMessage); begin KillTimer(HWindow, Timerld); if IsWindowEnabled(HWindow) then Show(sw_Minimize); end; Минимизация выполняется только по первому сообщению wm_Timer, при получении которого таймер становится не нужен и поэтому уничтожается функцией KillTimer. После этого с помощью функции IsWindowEnabled проверяется, доступно ли окно для ввода.
74 Программирование в среде Borland Pascal для Windows Если да, то оно минимизируется (вызывается метод Show с парамет- ром sw_Minimize). Если окно недоступно, то его минимизировать нельзя, иначе у пользователя могут возникнуть проблемы с переклю- чением на приложение. Смысл этой проверки станет понятнее, если вы создадите в программе модальное окно диалога (например, вызовом функции MessageBox). Если все сказанное выше понятно, не будет проблем и с полным текстом программы TIMERMSG: program TimerMsg; uses WinTypes, WinProcs {$ifdefVer80}, Messages {$endif}, OWindows; const Timerld = 1; { идентификатор таймера в программе } type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) procedure WMActivateApp(var Msg: TMessage); virtual wm_First + wm_ActivateApp; procedure WMTimer(var Msg: TMessage); virtual wm_First + wm_Timer; end; PTimerMsgApp = ATTimerMsgApp; TTimerMsgApp = object(TApplication) procedure InitMainWindow; virtual; end; { Устанавливает таймер на 5 с, если приложение утрачивает активность, и уничтожает его, если приложение получает активность } procedure TMainWindow.WMActivateApp(var Msg: TMessage); begin if Msg.WParam = 0 then SetTimer(HWindow, Timerld, 5000, nil) else KillTimer(HWindow, Timerld); DefWndProc(Msg); end; { По сообщению от таймера удаляет таймер и минимизирует окно } procedure TMainWindow.WMTimer(var Msg: TMessage); begin KillTimer(HWindow, Timerld); if IsWindowEnabled(HWindow) then Show(sw_Minimize); end; { Создает объект главного окна программы } procedure TTimerMsgApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Self Minimizing Window’)); end; var TimerMsgApp: TTimerMsgApp;
Гпава 3. Введение в Object Windows 75 begin TimerMsgApp.lnit(TimerMsg’); TimerMsgApp.Run; TimerMsgApp.Done; end. 3.12. УПОРЯДОЧЕННОЕ ЗАКРЫТИЕ ОКОН Когда вы выбираете в системном меню главного окна программы команду Close или нажимаете комбинацию клавиш Alt+F4, окно за- крывается и происходит выход из программы в среду Windows. Такое поведение является удобным только для простых программ, но для более сложных выглядит примитивным, поскольку при выходе из программы может оказаться, что пользователь не сохранил результа- тов своей работы. Хорошая прикладная программа просто обязана предложить пользователю сохранить наработанную информацию, если он не сделал этого сам. OWL имеет удобный механизм управления закрытием приложения и его окон. Когда вы пытаетесь закрыть приложение, у объекта Application вызывается виртуальный метод CanClose. Он возвращает значение типа Boolean, которое разрешает в данный момент завер- шить программу. В объекте TWindowsObject тоже объявлен вирту- альный метод CanClose, который всегда вызывается перед тем, как окно будет закрыто. По умолчанию метод CanClose объекта TApplication вызывает метод CanClose своего главного окна MainWindow. Для отмены завершения прикладной программы можно перекрыть CanClose в наследнике объекта TApplication. Однако в большинстве случаев объект главного окна сам решает, можно ли закрыть прило- жение. В объекте TWindowsObject метод CanClose вызывает CanClose у каждого дочернего окна и возвращает True только в том случае, когда все дочерние окна возвратили True. Если хотя бы одно окно возвра- тило False, он возвращает Fals6. Если CanClose оконного объекта возвращает True, то окно закрывается, а объект уничтожается. Если CanClose возвращает False, OWL игнорирует попытку закрыть окно. В приводимом ниже примере метод CanClose объекта TMainWindow просто выдает пользователю вопрос Do you really want to quit? (Вы действительно хотите выйти?) с возможностью ответа Yes или No (Да или Нет). Если пользователь отвечает утвердительно, про- исходит немедленный выход из программы в среду Windows. В про- тивном случае все остается без изменений, как если бы вообще не бы- ло попытки закрыть приложение. Изучите и выполните следующую программу, и все сказанное выше станет понятным:
76 Программирование в среде Borland Pascal для Windows program CloseWnd; uses WinTypes, WinProcs, OWindows; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) function CanClose: Boolean; virtual; end; PCIoseWndApp = ATCIoseWndApp; TCIoseWndApp = object(TApplication) procedure InitMainWindow; virtual; end; { Решает, можно ли закрыть окно } function TMainWindow.CanClose: Boolean; begin CanClose := False; if inherited CanClose then if MessageBox(HWindow, 'Do you really want to quit?', 'Quit', mb_YesNo + mbjconExclamation) = id_Yes then CanClose := True; end; { Создает объект главного окна программы } procedure TCIoseWndApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'Close Window')); end; var CloseWndApp: TCIoseWndApp; begin CloseWndApp. I nit('CloseWnd'); CloseWndApp.Run; CloseWndApp. Done; end. Обратите внимание на то, что TMainWindow.CanClose вызывает метод CanClose своего базового объекта. Благодаря этому активизи- руется механизм запроса удаления дочерних окон, реализованный в TWindowsObject.CanClose. Пользователю будет выдаваться пре- дупреждение о выходе только в том случае, если все дочерние окна главного окна “согласны” со своим уничтожением. Если хотя бы одно дочернее окно “опротестует” свое закрытие, удаление главного окна и всех его дочерних окон будет отменено. Итак, с окнами мы разобрались и в следующей главе рассмотрим, как выводить в них текстовую и графическую информацию,’ради которой, собственно, окна и строились.
4 ИНТЕРФЕЙС ГРАФИЧЕСКИХ УСТРОЙСТВ 4.1. ВВЕДЕНИЕ В GDI 4.1.1. GDI - ОСНОВА WINDOWS Любая Windows-программа выводит информацию на доступное гра- фическое устройство с помощью множества специальных функций, образующих так называемый GDI-интерфейс (от англ. Graphics Device Interface - интерфейс графических устройств). Рис. 4.1. Взаимодействие программа - GDI - графическое устройство Использование GDI предполагает качественно новый уровень взаимодействия прикладного программного обеспечения с устройст- вами графического вывода (рис. 4.1), поэтому программист должен знать принципы работы GDI и умело их использовать. Рассмотрим кратко важнейшие из них: • интерфейс GDI является аппаратно-независимым. Windows абст- рагирует вывод на графическое устройство от технических особенно- стей его работы. Поэтому программист работает с виртуальным уст- ройством вывода, которое может быть связано с любым физическим устройством (дисплеем, принтером, плоттером). Качество выводимо- го изображения определяется лишь физическими параметрами графи- ческого устройства: разрешающей способностью, максимальными размерами изображения, количеством одновременно отображаемых цветов. Windows аппроксимирует изображения, качество которых превосходит возможности графического устройства;
78 Программирование в среде Borland Pascal для Windows • независимость от устройств отображения достигается за счет ис- пользования программ-драйверов, которые преобразуют результат работы аппаратно-независимых функций GDI в команды, понятные электронному устройству. При смене видеоадаптера, принтера или плоттера не требуется модифицировать прикладные программы - достаточно лишь подключить в среде Windows соответствующий драйвер, который обеспечивает управление новым устройством; • в GDI все элементы графического изображения - точки, отрезки прямых, геометрические фигуры - описываются в рамках принятой логической системы координат, которая может отличаться от физиче- ской, используемой устройством вывода. Изображение на экране яв- ляется плоским, поэтому GDI использует двухмерную систему коор- динат, похожую на декартову. Эта система координат характеризует; ся двумя перпендикулярными осями X и Y. Ось X считается горизон- тальной, а ось У - вертикальной. Значения координат по обеим осям изменяются в пределах от -32768 до 32767. Указанное ограничение на значения координат является следствием того, что GDI представляет координаты целыми числами типа Integer. Относительное положение начала координат, ориентация осей X и У и масштаб выводимого изображения могут регулироваться. GDI предоставляет в распоряже- ние программиста восемь различных режимов отображения, позво- ляющих ему рисовать в долях миллиметра, в долях дюйма, в пунктах (1/1440 дюйма), в логических единицах, которые имеют заданное со- отношение с единицами устройства вывода. По умолчанию логиче- ская система координат совпадает с физической: координаты задают- ся в пикселах, ось X направлена вправо, а ось У - вниз; • цвет точек изображения формируется на основе трех основных цветов: красного, зеленого, синего (Red, Green, Blue), каждому из ко- торых отводится по одному байту. Такое кодирование цвета позволя- ет хранить и обрабатывать изображения самого высокого качества (до 16 млн одновременно отображаемых оттенков!); • GDI-графика позволяет строить пользовательский интерфейс по принципу WYSIWYG (What You See Is What You Get - что видим, то и получаем). Это полезное свойство обеспечивается применением в Windows 3.1/95 масштабируемых шрифтов TrueType. Теперь рассмотрим, как эти принципы практически реализуются в программах для Windows. 4.1.2. КОНТЕКСТ УСТРОЙСТВА В основе работы GDI с графическими устройствами лежит понятие контекста устройства (от англ. Device Context - DC). Контекст уст- ройства (контекст отображения) - это логический объект системы Windows, который связан с физическим устройством и заменяет его в
Гпава 4. Интерфейс графических устройств 79 функциях вывода. С «помощью контекстов отображения Windows обеспечивает универсальное использование всех устройств графиче- ского вывода, независимо от их технических характеристик. Для указания того, куда именно осуществляется вывод, первым па- раметром в функцию GDI передается дескриптор соответствующего контекста устройства. С точки зрения прикладной программы кон- текст устройства - это виртуальный экран с необходимыми атрибу- тами и средствами рисования, такими как “перо”, “кисть”, шрифт, цвет фона, цвет текста, текущая позиция рисования и др. Когда функция GDI строит изображение в контексте устройства, связанный с ним драйвер преобразует операции рисования в команды конкретного физического устройства, которое интерпретирует вход- ные команды и воспроизводит изображение настолько точно, на- сколько это возможно. Самым распространенным устройством вывода является дисплей, поэтому в большинстве случаев мы будем говорить о контексте дис- плея. Контекст дисплея позволяет интерпретировать каждое окно прикладной программы как устройство отображения. Приложение, которое запрашивает контекст дисплея для конкретного окна, полу- чает управление экраном внутри этого окна и не может осуществлять доступ за его пределы. Так как информация чаще всего выводится в окно, программист должен знать особенности создания изображений в окнах, которые мы и рассмотрим ниже. 4.2. ВЫВОД ИНФОРМАЦИИ В ОКНО 4.2.1. ОКНО КАК ОБЪЕКТ ВЫВОДА Дисплей является одним из устройств, взаимодействие с которым рег- ламентируется GDI-интерфейсом. Стратегия управления дисплеем в Windows отличается от той, которая используется при разработке графических программ в DOS. DOS-приложения распоряжаются эк- раном дисплея монопольно, это связано с тем, что DOS не имеет соб- ственных средств вывода графики. DOS-программа может либо на- прямую обращаться к видеоадаптеру и адресовать видеопамять, либо использовать различные графические библиотеки. 0 В Windows дело обстоит иначе. Программы пользователя полно- стью изолированы от обращений к аппаратуре. В связи с тем, что Windows поддерживает одновременное исполнение нескольких при- ложений, дисплей, на который происходит вывод, является разделяе- мым устройством. Windows осуществляет полный контроль за ис- пользованием этого ресурса, обеспечивая каждое приложение своей областью отображения. Разделение дисплея между приложениями
80 Программирование в среде Borland Pascal для Windows осуществляется с помощью окон, которые могут менять размеры, перемещаться и перекрывать друг друга. Всю выводимую на экран информацию приложения направляют в связанные с ними окна. Во время выполнения программы видимая площадь окна может изменяться: перекрываться полностью или частично другим окном, исчезать за границами экрана, раскрываться на весь экран и т.д. Это требует постоянного контроля за отображаемой на экране информа- цией и правильного восстановления утрачиваемых частей изображе- ния. Однако Windows не хранит графическую копию каждого окна - это было бы слишком расточительно с точки зрения использования оперативной памяти компьютера. Windows возлагает ответственность за правильное отображение окна на прикладную программу, посылая ей сообщение с кодом wm_Paint каждый раз, когда все окно или его часть требует перерисовки. Необходимость перерисовки Windows определяет сама. В ответ на сообщение wm_Paint программа восста- навливает утраченную часть окна собственными средствами. Как Windows определяет, что именно и когда нужно восстанавли- вать? При операциях с окнами система помечает разрушенные части окна как подлежащие обновлению и помещает информацию о них в специальную область, называемую областью обновления (update region). На основании содержимого этой области и происходит вос- становление. Windows посылает окну сообщение wm_Paint всякий раз, когда область обновления окна оказывается непустой, и при условии, что в очереди приложения нет других адресованных окну сообщений. При получении сообщения wm_Paint окно должно перерисовать только свою внутреннюю часть, называемую, как вы помните, рабочей областью (client area). Отображение тех частей окна, которые не отно- сятся к рабочей области (рамка, заголовок, кнопки минимизации, максимизации и системного меню, полосы скроллинга, а также меню окна), Windows берет на себя. Однако окно всегда может определить свой стиль рисования перечисленных элементов. Для этого ему доста- точно перехватывать и обрабатывать сообщение wm_NCPaint, кото- рое посылается системой Windows при необходимости перерисовать системные (нерабочие) области окна (non-client area). 4.2.2. ОТСЕЧЕНИЕ Скорость работы графики всегда оставалась “головной болью” раз- работчиков, заставляющей искать и находить эффективные решения при создании графических систем. В Windows этому вопросу тоже уделено достойное внимание. Представьте себе, что происходило бы на экране, если бы каждое окно перерисовывало все свое содержимое при любом, даже незначи- тельном изменении его видимой области. На экран было бы просто неприятно смотреть! Поэтому для ускорения графического вывода
Гпава 4. Интерфейс графических устройств 81 Windows оптимизирует перерисовку, осуществляя отсечение (clipping). В результате отсечения на экране перерисовываются лишь те области окна, которые действительно требуют обновления. Вывод за грани- цами области отсечения игнорируется. Это дает право прикладной программе в ответ на сообщение wm_Paint перерисовывать всю рабо- чую область окна, предполагая, что Windows отсечет лишний вывод. 4.2.3. РЕАКЦИЯ НА СООБЩЕНИЕ wm_Paint Чтобы научить окно отображать свое содержимое, достаточно опре- делить в нем реакцию на сообщение wm_Paint. Окно должно выпол- нить следующие действия: • вызвать функцию BeginPaint, которая подготавливает указанное окно для рисования. Она определена в WINPROCS как BeginPaint(Wnd: HWnd; var Paint: TPaintStruct): HDC; где Wnd - окно, которое будет перерисовываться; Paint - запись, в которую помещается дополнительная информация для рисования. При успешном выполнении функция возвращает дескриптор контек- ста дисплея, ассоциированного с окном Wnd; ® с помощью функций GDI вывести требуемую информацию в ок- но. При вызове этих функций в качестве параметра, обозначающего контекст устройства, необходимо передавать дескриптор контекста дисплея, который был возвращен функцией BeginPaint; • завершить рисование через вызов процедуры EndPaint, которая освобождает контекст дисплея для его повторного использования другими окнами: EndPaint(Wnd: HWnd; var Paint: TPaintStruct); где Wnd - окно, в которое осуществлялся вывод; Paint - дополнитель- ная информация для рисования, полученная вызовом функции BeginPaint. При работе с библиотекой ObjectWindows задача упрощается. Чтобы заставить окно перерисовывать себя, достаточно в наследнике объекта TWindow перекрыть виртуальный метод Paint: TMyWindow = object (TWindow) о procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; По сообщению wm_Paint произойдет автоматический вызов функ- ции BeginPaint, затем виртуального метода Paint и, наконец, функции EndPaint. Параметр PaintDC при вызове метода Paint содержит кон- текст дисплея, возвращенный функцией BeginPaint, а параметр Paintinfo точно соответствует параметру Paint в функциях BeginPaint
82 Программирование в среде Borland Pascal для Windows и EndPaint. При использовании OWL вы никогда не должны явно вызывать BeginPaint и EndPaint. Представление о работе метода Paint дает программа PAINT: program Paint; uses WinTypes, WinProcs, OWindows; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; PPaintApp = ATPaintApp; TPaintApp = object(TApplication) procedure InitMainWindow; virtual; end; { Перерисовывает рабочую область окна } procedure TMainWindow.Paint ^PaintDC: HDC; var Paintinfo: TPaintStruct); var R: TRect; begin GetClientRect(HWindow, R); Ellipse(PaintDC, R.Left, R.Top, R.Right, R.Bottom); end; { Создает объект главного окна программы } procedure TPaintApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, Name)); end; var PaintApp: TPaintApp; begin PaintApp.lnit(’Paint’); PaintApp.Run; PaintApp. Done; end. Скомпилируйте и запустите эту программу. На экране появится окно, в котором будет нарисован эллипс, вписанный в рабочую об- ласть. При изменении размеров окна будут соответственно изменяться и размеры эллипса. Столь впечатляющее для новичков зрелище обес- печивает метод Paint объекта TMainWindow, который осуществляет вызов двух функций Windows API - GetClientRect и Ellipse: procedure TMainWindow.Paint (PaintDC: HDC; var Paintinfo: TPaintStruct); var R: TRect; begin GetClientRect(HWindow, R); Ellipse(PaintDC, R.Left, R.Top, R.Right, R.Bottom); end;
Глава 4. Интерфейс графических устройств 83 Чтобы нарисовать эллипс, вписанный в рабочую область окна, требуется узнать ее размеры с помощью процедуры GetClientRect(Wnd: HWnd; var Rect: TRect); где Wnd - окно, у которого запрашиваются координаты рабочей об- ласти; Red - запись типа TRect, в которую помещаются координаты рабочей области. Поля Left и Тор в этой записи (координаты левого верхнего угла) будут равны нулю, а поля Right и Bottom будут содер- жать соответственно ширину и высоту окна. Нетрудно заметить, что в программе PAINT размеры рабочей об- ласти окна определяются при каждой прорисовке. Это делается пото- му, что, во-первых, окно “не знает” заранее размеров своей рабочей области, а во-вторых, нельзя считать, что размеры этой области ос- таются неизменными в процессе работы программы. С помощью функции Ellipse метод Paint рисует вписанный в рабо- чую область эллипс. Функция Ellipse имеет следующий формат: Ellipse(DC: HDC; XI, Yl, Х2, Y2: Integer): Bool; где DC - контекст устройства; XI, Y1 - логические координаты левого верхнего угла прямоугольника, ограничивающего эллипс; Х2, Y2 - логические координаты нижнего правого угла прямоугольника, огра- ничивающего эллипс. В случае успеха функция возвращает ненулевое значение. Границы эллипса рисуются с использованием текущего “пера”, его внутренняя область заполняется с использованием теку- щей “кисти” (что такое “перо” и “кисть” - вы узнаете позже). Если запустить теперь программу PAINT под отладчиком Turbo Debugger for Windows, можно обнаружить некоторые интересные особенности работы графической системы Windows. Установите точ- ку останова (breakpoint) на метод Paint и “поиграйте” с окном: изме- ните его размеры, перенесите в другое место экрана, попробуйте пере- крыть это окно другим окном, сверните в пиктограмму, разверните на весь экран, вызовите его системное меню. В результате анализа пове- дения окна можно сделать следующие выводы: • сразу после запуска окно программы получает сообщение wm_Paint, что позволяет ему отобразить свое содержимое; • при простом передвижении окна Windows его не перерисовывает; • при изменении размеров и в других случаях изменения видимой площади окна Windows посылает ему сообщение wm_Paint, требуя перерисовать свое содержимое; • при открытии и закрытии меню метод Paint не вызывается. Это объясняется тем, что Windows создает битовую копию закрываемых областей окна, чтобы заставить меню работать с максимальной скоростью.
84 Программирование в среде Borland Pascal для Windows 4.2.4. ПРИНУДИТЕЛЬНАЯ ПЕРЕРИСОВКА ОКНА Инициатором сообщения wm_Paint может выступать не только систе- ма Windows, но и сама прикладная программа. Для того чтобы спро- воцировать посылку своему окну сообщения wm_Paint, программа должна пометить часть рабочей области окна, как “неверную” (invalid), т.е. требующую обновления. Это можно сделать с помощью функции InvalidateRect, добавляющей к области обновления окна заданную прямоугольную область: InvalidateRect(Wnd: HWnd; Rect: PRect; Erase: Bool); где Wnd - окно, у которого изменяется область обновления; Red - указатель на запись, содержащую координаты прямоугольника, до- бавляемого к области обновления (если этот параметр равен nil, то к области обновления добавляется вся рабочая область окна);? Erase - определяет, нужно ли очищать область обновления перед перерисов- кой. После вызова InvalidateRect окно не будет перерисовано сразу. Перерисовка произойдет только при опросе программой своей очере- ди сообщений. Причем если в очереди окажутся другие сообщения, то сначала будут обработаны они. Когда прикладная очередь опустеет, окно получит сообщение wm_Paint, в ответ на которое оно сможет обновить свою рабочую область. Как видим, сообщение wm_Paint обладает наименьшим приорите- том по сравнению с остальными сообщениями. Это обстоятельство положительно влияет на скорость работы системы Windows в целом, избавляя приложения от лишних и частых операций перерисовки, особенно, если на экране отображается много перекрывающихся окон. Однако в определенных случаях такая запоздалая перерисовка окна может оказаться совершенно неудовлетворительной. Если при- ложению требуется немедленно перерисовать рабочую область окна, то оно может сделать это вызовом функции UpdateWindow(Wnd: HWnd); где Wnd - обновляемое окно. Функция UpdateWindow немедленно посылает окну сообщений wm_Paint, минуя очередь приложения. Об- ласть обновления окна в момент вызова функции должна быть непус- той, иначе сообщение wm_Paint послано не будет. Участок программы, который организует немедленную перерисов- ку всей рабочей области окна, выглядит так: { Пометить к обновлению всю рабочую область окна } lnvalidateRect(HWindow, nil, True); { Немедленно обновить окно } UpdateWindow(HWindow);
Гпава 4. Интерфейс графических устройств 85 Сам процесс рисования (т.е. обращение к функциям GDI) может производиться не только в ответ на сообщение wm_Paint, но и в лю- бой момент, когда это необходимо, например при анимации изобра- жений по сообщению wm_Timer. Последовательность выполняемых действий аналогична описанной выше, однако получение и освобож- дение контекста дисплея осуществляется иначе. Для получения контекста дисплея вызывается функция GetDC(Wnd: HWnd): HDC; где Wnd - дескриптор окна, для которого запрашивается контекст дисплея. Функция возвращает контекст дисплея, ассоциированный с окном Wnd. Заметим, что количество контекстов дисплея, которые Windows может одновременно предоставить таким образом, ограни- чено и равно пяти. Освобождение использованного контекста дисплея выполняется с помощью функции ReleaseDC(Wnd: HWnd; DC: HDC): Integer; где Wnd - дескриптор окна, с которым связан освобождаемый кон- текст дисплея; DC - освобождаемый контекст дисплея. В результате вызова функции контекст дисплея становится доступным для исполь- зования другими приложениями. Вызовы функций GetDC и ReleaseDC должны быть сбалансированы, иначе возникнут серьезные проблемы при работе программы. Приведенная ниже программа RANDRECT демонстрирует рисо- вание вне метода Paint. Она выводит в случайное место своего окна квадраты размерами 20x20 по сообщениям от таймера: program RandRect; uses WinTypes, WinProcs {$ifdefVer80}, Messages {$endif}, OWindows; const Timerld = 1; {-идентификатор таймера в программе } type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) procedure SetupWindow; virtual; procedure WMDestroy(var Msg: TMessage); virtual wnri_First + wm_Destroy; 0 procedure WMTimer(var Msg: TMessage); virtual wnri_First + wm_Timer; end; PRandRectApp = ATRandRectApp; TRandRectApp = object(TApplication) procedure InitMainWindow; virtual; end;
86 Программирование в среде Borland Pascal для Windows { После создания окна устанавливает таймер } procedure TMainWindow.SetupWindow; begin inherited SetupWindow; SetTimer(HWindow, Timerld, 50, nil); end; { Удаляет таймер при разрушении окна } procedure TMainWindow.WMDestroy(var Msg: TMessage); begin KillTimer(HWindow, Timerld); inherited WMDestroy(Msg); end; { По сообщению от таймера рисует квадрат в случайном месте окна } procedure TMainWindow.WMTimer(var Msg: TMessage); var DC: HDC; R: TRect; P: TPoint; begin DC := GetDC(HWindow); GetClientRect(HWindow, R); P.X := Random(R.Right - 20); P.Y := Random(R. Bottom - 20); Rectangle(DC, P.X, P.Y, P.X + 20, P.Y + 20); ReleaseDC(HWindow, DC); end; { Создает объект главного окна программы } procedure TRandRectApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Random Rectangles’)); end; var RandRectApp: TRandRectApp; begin RandRectApp.lnit(’RandRect’); RandRectApp. Run; RandRectApp. Done; end. В этой программе используется таймер. Лучшее место для его ус- тановки - метод SetupWindow оконного объекта, поскольку он вызы- вается сразу после создания интерфейсного элемента (когда дескрип- тор HWindow получил реальное значение). В методе SetupWindow происходит установка таймера с идентификатором Timerld на интер- вал времени 50 миллисекунд. Для этого вызывается функция SetTimer. Лучшее место для уничтожения таймера - метод WMDestroy, который вызывается по сообщению wm_Destroy при разрушении окна. Для уничтожения таймера с идентификатором Timerld метод WMDestroy вызывает функцию KillTimer.
Гпава 4. Интерфейс графических устройств 87 Обработку сообщений таймера выполняет метод WMTimer, кото- рый запрашивает для окна контекст дисплея, а затем рисует квадрат размерами 20x20, выбирая его местоположение случайным образом в пределах рабочей области. Рисование квадрата осуществляется с по- мощью функции Rectangle(DC: HDC; XI, Yl, Х2, Y2: Integer): Bool; где DC - контекст устройства; XI, Y1 - координаты левого верхнего угла прямоугольника; Х2, Y2 - координаты нижнего правого угла прямоугольника. После рисования контекст дисплея освобождается. Важно помнить, что при рисовании вне метода Paint (например, по нажатии кнопки мыши или при поступлении сообщения от таймера) Windows не гарантирует сохранение выведенной информации при изменении видимой части окна. Например, если окно программы RANDRECT, где нарисованы прямоугольники, закрыть другим ок- ном, а затем снова открыть, то потерянная часть изображения не вос- становится. Чтобы нарисованное изображение не терялось при опера- циях с окнами, окно должно запоминать выведенную в него информа- цию и восстанавливать изображение по сообщению wm_Paint. Хране- ние выведенной информации может быть организовано произволь- ным образом. 4.3. УПРАВЛЕНИЕ ЦВЕТОМ Как уже говорилось выше, Windows предоставляет приложениям ап- паратно-независимый способ кодирования цвета. Настало время рас- смотреть вопрос управления цветом более подробно. Цвет в Windows задается в логическом формате RGB, т.е. тройкой чисел, определяющих степени интенсивности трех его цветовых со- ставляющих - красной, зеленой и синей (Red, Green, Blue). Для коди- рования цвета в модуле WINTYPES объявлен тип данных TColorRef, который эквивалентен длинному целому (Longint). Очевидно, что переменная, объявленная с типом TColorRef, занимает в памяти 4 байта. Самый старший байт TColorRef зарезервирован и должен быть равен 0, а три младших байта содержат степени интенсивности крас- ной, зеленой и синей составляющих (рис. 4.2). * Получить составной цвет по значениям трех его компонентов можно вызовом функции RGB: Color := RGB(255, 0, 0); {ярко-красный цвет } Значение функции RGB имеет тип TColorRef и может передаваться как параметр в любую функцию GDI, принимающую цвет.
88 Программирование в среде Borland Pascal для Windows Старший байт 4 байта Младший байт Красный (Red) Зеленый (Green) Синий (Blue) Зарезервировано Рис. 4.2. Кодирование цвета в формате RGB Если требуется выделить из цвета одну из его составляющих, то это можно сделать функциями GetRValue, GetGValue, GetBValue. Всё функции возвращают значение типа Byte. Например, чтобы выделить красную составляющую из цвета, можно записать: RedValue := GetRValue(Color); { RedValue принадлежит типу Byte } Нетрудно подсчитать, что описанный способ кодирования позво- ляет иметь в программе одновременно до 16 миллионов оттенков (если точно, то 16777216). Конечно, далеко не все графические устройства поддерживают такое количество цветов. Поэтому, если программа устанавливает цвет, который данное устройство воспроиз- вести не может, Windows заменяет его на ближайший из числа доступ- ных. На практике часто требуется знать, какой цвет из существующих является наиболее близким, “по мнению” Windows, к указанному ва- ми. Для этого используется функция GetNearestColor: Color := GetNearestColor(DC, RGB(23, 71, 240)); Функция GetNearestColor подбирает на основе логического цвета из диапазона 16 млн оттенков ближайший логический цвет, который может воспроизвести устройство, связанное с DC. Обычно техника программирования при работе с цветами предпо- лагает получение, а затем использование ближайшего цвета для за- данного произвольного. Контекст устройства поддерживает понятия фона и фонового цвета. Некоторые функции GDI могут осуществлять предваритель- ную заливку области, в которой осуществляется рисование, фоновым цветом. Фоновый цвет используется, например, при выводе текста. В режиме использования фона Opaque функции вывода текста очищают фоновым цветом ячейку для каждого выводимого символа. Альтерна- тивным является режим Transparent, в котором текст формируется на уже существующем фоне.
Гпава 4. Интерфейс графических устройств 89 Функция GetBkColor позволяет получить, а функция SetBkColor - установить цвет фона в контексте дисплея: OldColor := GetBkColor(DC); SetBkColor(DC, RGB(127, 127, 127)); В данном фрагменте мы запоминаем предыдущий фоновый цвет и устанавливаем новый. 4.4. ГРАФИЧЕСКИЕ СРЕДСТВА РИСОВАНИЯ 4.4.1. ПЕРЬЯ, КИСТИ, ШРИФТЫ Контекст устройства поддерживает три типа средств для выполнения рисования: “перо”, “кисть” и шрифт. Перо предназначено для рисова- ния линий, дуг, ломаных и границ замкнутых фигур. Кисть использу- ется при заполнении внутреннего пространства замкнутых фигур, таких как прямоугольники; многоугольники и эллипсы. Шрифт оп- ределяет особенности начертания текста. Каждое средство рисования (перо, кисть и шрифт) представлено в контексте устройства объектом соответствующего типа. В модуле WINTYPES определены дескрип- торные типы указанных объектов: type HPen = THandle; {дескриптор пера } HBrush = THandle; {дескриптор кисти} HFont = THandle; { дескриптор шрифта } Атрибуты контекста устройства, в том числе и средства рисования (перо, кисть и шрифт), можно рассматривать как его элементы дан- ных. Приложениям запрещено обращаться к ним напрямую. Для по- лучения и установки атрибутов контекста устройства используются специальные функции Windows. Перья, кисти и шрифты никогда не передаются явно в функции ри- сования. GDI всегда использует перо, кисть и шрифт, подключенные к контексту устройства, в который направляется вывод. Передача ри- сующей функции дескриптора контекста устройства означает неявную передачу этой функции всех параметров виртуальной поверхности вместе со всеми ее атрибутами, в том числе и средствами рисования. В контексте устройства доступны (выбраны) одновременно одно перо, одна кисть и один шрифт. Например, чтобы рйсовать границы фигур другими стилем и цветом, необходимо сменить в контексте устройства перо, а для того, чтобы изменить стиль или цвет заполне- ния внутреннего пространства этих фигур, нужно сменить еще и кисть. Заменой одного GDI-объекта на другой управляет функция SelectObject(DC: HDC; hObject: THandle): THandle;
90 Программирование в среде Borland Pascal для Windows где DC - контекст устройства; hObject - устанавливаемый графиче- ский объект. После вызова функции SelectObject в контексте устройства DC устанавливается объект, заданный параметром hObject. Значением функции является дескриптор объекта, который был установлен до этого. Обратите внимание на то, что функция SelectObject использует- ся для установки GDI-объектов различных типов - перьев, кистей, шрифтов. Она сама определяет тип переданного объекта и заменяет старый объект такого же типа на новый, переданный в параметре hObject (перо заменяется пером, кисть кистью и т.д.). Например, опе- ратор OldBrush := SelectObject(DC, NewBrush); заменяет “старую” кисть OldBrush на “новую” NewBrush. Если после этого вызвать функцию SelectObject еще раз, с параметром OldBrush, то прежняя кисть будет восстановлена. Вообще говоря, с помощью функции SelectObject можно заменить не только средство рисования, но и любой другой объект контекста устройства, например область отсечения, растровое изображение (точнее поверхность отображения). Замена последнего возможна лишь в том случае, когда поверхностью отображения является не эк- ран дисплея, а область оперативной памяти (см. 5.8.3). 4.4.2. СОЗДАНИЕ И УДАЛЕНИЕ СРЕДСТВ РИСОВАНИЯ Прикладная программа может создавать графические объекты с тре- буемыми свойствами и удалять их. Создать, например, перо можно вызовом функции CreatePen, а шрифт - вызовом CreateFont (они бу- дут рассмотрены позже). Обычно следующим шагом после создания графического объекта является его подключение к используемому контексту устройства с помощью функции SelectObject. Когда потреб- ность в объекте исчезает, он удаляется вызовом функции DeleteObject(Handle: THandle): Bool; где Handle - удаляемый объект. Функция DeleteObject удаляет указан- ный объект, освобождая всю занимаемую им системную память. Уда- ляемым объектом может быть перо, кисть, шрифт, растровое изобра- жение, область или палитра. В случае успеха DeleteObject возвращает True. Необходимо помнить, что приложение не должно удалять объект, который установлен в текущий момент в контексте устройства. Преж- де чем удалить такой объект, в контексте устройства следует устано- вить другой объект такого же типа. Лучше всего восстановить перво- начальный объект:
Гпава 4. Интерфейс графических устройств 91 {Восстанавливаем в контексте устройства первоначальный объект} Brush := SelectObject(DC, OldBrush); { Удаляем объект, который стал не нужен } DeleteObject(Brush); После вызова функции DeleteObject дескриптор переданного ей объекта становится недействительным. Кроме динамического создания GDI-объектов, существует воз- можность использовать предопределенные объекты, которые Windows предоставляет в распоряжение каждого приложения. Полу- чить дескриптор предопределенного объекта (пера, кисти или палит- ры цветов) можно с помощью функции GetStockObject(Index: Integer): THandle;. где Index -«индекс в массиве предопределенных объектов Windows (табл. 4.1). Табл. 4.1. Имена констант для функции GetStockObject Константа Описание Black_Brush Черная кисть DkGray_Brush Темно-серая кисть Gray_Brush Серая кисть Null_Brush, Hollow_Brush Невидимая кисть LtGray_Brush Светло-серая кисть White_Brush Белая кисть Black_Pen Черное перо Null.Pen Невидимое перо White_Pen Белое перо ANSI_Fixed_Font Непропорциональный системный шрифт Windows ANSI_Var_Font Пропорциональный системный шрифт Windows Device_Default_Font Зависимый от устройства шрифт OEM_Fixed_Font OEM-зависимый непропорциональный шрифт System_Font Системный шрифт. По умолчанию Windows ис- пользует системный шрифт для вывода пунктов меню, текста управляющих элементов в диалого- вых блоках, других системных Областей System_Fixed_Font Непропорциональный системный шрифт. Он существует для поддержания совместимости с ранними версиями системы Windows Default_Palette Цветовая палитра по умолчанию. Данная палитра состоит из неизменных цветов системной палитры
92 Программирование в среде Borland Pascal-для Windows Объект, полученный вызовом функции GetStockObject, можно ис- пользовать так, как будто он был создан приложением, за одним ис- ключением: его нельзя удалять функцией DeleteObject. Создание и удаление предопределенных объектов относится исключительно к компетенции системы Windows. Предопределенные объекты можно устанавливать в контексте уст- ройства функцией SelectObject. Когда потребность в них исчезает, не забывайте восстанавливать контекст устройства в прежнее состояние. Следующий фрагмент программы показывает, что нужно сделать, чтобы рисовать стандартной кистью со сплошной черной заливкой: Brush := GetStockObject(Black_Brush); OldBrush := SelectObject(DC, Brush); { Рисование кистью } SelectObject(D’C, OldBrush); При работе с графическими объектами вам может понадобиться узнать параметры, с которыми они были созданы. Для этой цели слу- жит функция GetObject, которая переносит информацию об объекте в предоставленный программой буфер: GetObject(hObject: THandle; Count: Integer; IpObject: Pointer): Integer; где hObject - дескриптор пера, кисти, шрифта, растрового изображе- ния или палитры; Count - объем буфера для данных; IpObject - указа- тель на приемный буфер. Приемным буфером может быть запись типа TLogPen (перо), TLogBrush (кисть), TLogFont (шрифт) или TBitmap (растровое изображение). Упомянутые типы данных будут рассмотре- ны позже. 4.5. ВЫВОД ТЕКСТА Говоря объективно, вывод графического текста со множеством оттен- ков, форм и “завитушек”, да еще в масштабе - дело чрезвычайно сложное. К счастью, решение этой проблемы скрыто от пытливого ума прикладного программиста в недрах Windows, он пользуется го- товыми результатами, и процесс этот не намного сложнее, чем в MS-DOS. Windows имеет богатейший набор средств вывода текста, важнейшим элементом которого являются шрифты, в частности мас- штабируемые шрифты TrueType. Программисту остается лишь ука- зать, каким шрифтом что выводить. Начнем с самого простого - выведем на экран строку текста. Ис- пользуем для решения этой простой задачи функцию
Гпава 4. Интерфейс графических устройств 93 TextOut(DC: HDC; X, Y: Integer; Str: PChar; Count: Integer): Bool; где DC - контекст устройства; X, Y - логические координаты начала вывода; Str - выводимая строка; Count - число символов в строке. В случае успешного выполнения функция возвращает True. Функция TextOut одинаково пригодна для вывода и нуль- терминированных строк языка С, и строк языка Pascal. Это самая простая функция вывода текста в Windows и по своему формату она похожа на функцию OutTextXY графической библиотеки Borland для MS-DOS (BGI). В следующей программе функция TextOut использует- ся для отображения в окне двух текстовых строк (строки формата String и нуль-терминированной строки): program TxtOut; uses WinTypes, WinProcs, OWindows, Strings; type PMainWindow = ATMainWindow; TMain Window = object(TWindow) procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; PTxtOutApp = ATTxtOutApp; TTxtOutApp = object(TApplication) procedure InitMainWindow; virtual; end; { Выводит в окно две текстовые строки } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); const PasStr: String = 'Pascal string'; CStr: PChar = "’C string'; begin TextOut(PaintDC, 50, 50, @PasStr[1], Length(PasStr)); TextOut(PaintDC, 50, 100, CStr, StrLen(CStr)); end; { Создает объект главного окна программы } procedure TTxtOutApp. InitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'Text Output')); end; var TxtOutApp: TTxtOutApp; begin * TxtOutApp.lnit('TxtOut'); TxtOutApp.Run; TxtOutApp.Done; end. Существуют и другие, более широкие по возможностям, функции вывода текста: ExtTextOut, TabbedTextOut, DrawText и GrayString.
94 Программирование в среде Borland Pascal для Windows Для вывода текста в прямоугольную область и построения таблиц удобно использовать функцию DrawText(DC: HDC; Str: PChar; Count: Integer; var Rect: TRect; Format: Word): Integer; где DC - контекст устройства отображения; Str - выводимая строка; Count - число отображаемых символов строки (если этот параметр равен -1, то предполагается, что строка заканчивается нулем); Rect - координаты прямоугольной области, в рамках которой отображается строка Str; Format - набор битовых флагов, определяющих способ форматирования строки при выводе (табл. 4.2). При успешном вы- полнении функция DrawText возвращает высоту выведенного текста. Табл. 4.2. Флаги функции DrawText Константа Описание dt_Top Выравнивает текст по верхней границе прямоугольника; действует только тогда, когда весь текст выводится в одну строку (dt_SingleLine) dtJLeft dt_Center dt_Right dt_VCenter dt_Bottom Выравнивает текст по левой границе прямоугольника Центрирует текст горизонтально Выравнивает текст по правой границе прямоугольника Центрирует текст вертикально Выравнивает текст по нижней границе прямоугольника; действует только тогда, когда весь текст выводится в одну строку (dt_SingleLine) dt_WordBreak Разрешает автоматический перенос слов на новую стро- ку при попадании текста за границы прямоугольника. Если в тексте встречаются последовательности # 13# 10 (#13 - возврат каретки, #10 - перевод строки), то они тоже переводят строку dt_SingleLine dt_ExpandTabs Выводит весь текст в одну строку Заменяет встречающиеся символы табуляции на пробелы (по умолчанию на 8 пробелов) dt_TabStop Устанавливает величину отступа для символа табуляции. Величина отступа определяется старшим байтом пара- метра Format. Флаг dt_TabStop не может использоваться совместно с флагами, описанными ниже dt_NoClip Отменяет отсечение при выводе текста dt_ExtemalLeading Включает в высоту текста верхний отступ шрифта (запас на высоту таких букв, как б, й и т.д.) dt_CalcRect Пересчитывает ограничивающий прямоугольник, чтобы в нем умещался весь текст, но реально текст не выводит
Гпаеа 4. Интерфейс графических устройств 95 Окончание табл. 4.2 Константа Описание dt_No Prefix Отменяет интерпретацию префиксного символа &. Если этот флаг не установлен, то встречающийся при выводе символ & превращается в подчеркивание следующего за ним символа, а последовательность && - в символ & dtjnternal Включает в высоту текста нижний отступ шрифта (запас на высоту таких букв, как р, у и т.д.) Одно обращение к функции DrawText обеспечивает вывод одной ячейки таблицы. Использование DrawText покажем на примере сле- дующей программы: program DrawTxt; uses WinTypes, WinProcs, OWindows; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; PDrawTxtApp = ATDrawTxtApp; TDrawTxtApp = object(TApplication) procedure InitMainWindow; virtual; end; const { Текст, выводимый в табличной форме } DemoText: array [1..3] of PChar = ( 'I am left aligned text with word breaking.', { текст 1-й ячейки } 'I am right aligned text with word breaking.', { текст 2-й ячейки } 'I am centered text with word breaking.'); { текст 3-й ячейки} { Флаги форматирования текста } DemoFormat: array [1..3] of Word = ( dt_Left or dt_WordBreak, dt_Right or dt_WordBreak, dt_Center or dt_Word Break); { Рисует в окне таблицу из трех ячеек, содерх^ащих выровненный текст } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var R: TRect; I: Integer; begin SetRect(R, 50, 50, 150, 150); for I := 1 to 3 do begin Rectangle(PaintDC, R.Left, R.Top, R.Right, R.Bottom); lnflateRect(R, -1, -1); DrawText(PaintDC, DemoText[l], -1, R, DemoFormat[l]); lnflateRect(R, 1,1);
96 Программирование в среде Borland Pascal для Windows OffsetRect(R, R.Right - R.Left -1,0); end; end; { Создает объект главного окна программы } procedure TDrawTxtApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'DrawText Demo')); end; var DrawTxtApp: TDrawTxtApp; begin DrawT xtApp. I nitfDrawTxt'); DrawTxtApp. Run; DrawT xtApp. Done; end. В программе DRAWTXT в окно приложения выводится таблица, которая состоит из одной строки, разделенной на три колонки. В пер- вой колонке текст выравнивается по левому краю прямоугольника, во второй - по правому краю, а в третьей - горизонтально центрируется. Во всех трех колонках осуществляется перенос слов на новую строку при выходе текста за границы прямоугольника. Чтобы подчеркнуть границы прямоугольной области, в которую выводится текст, ее сле- дует обвести тонкой сплошной линией с помощью функции Rectangle. Мы не будем останавливаться на более сложных в применении функциях вывода текста. Их описание вы найдете в справочнике по Borland Pascal. 4.6. УПРАВЛЕНИЕ ШРИФТАМИ В современном программировании уже недостаточно просто вывести текст на экран, надо вывести его красиво! Для этого используются шрифты. На первый взгляд шрифт должен указываться как параметр в функции вывода текста, однако в Windows шрифт задается иначе. Особенности начертания символов определяются графическим объек- том “шрифт” используемого контекста устройства. Шрифт представляет собой множество символов, имеющих сход- ные размеры и начертание контуров. Можно утверждать, что концеп- ция шрифтов в Windows, особенно TrueType, выходит за рамки GDI-интерфейса и обладает определенной самостоятельностью. Технология TrueType предполагает возможность переноса шрифтов между любыми графическими устройствами и даже операционными средами. Управление шрифтами в прикладной программе - дело не столько трудное, сколько кропотливое; кроме того, необходимость манипуля- ции шрифтами возникает далеко не во всех приложениях. Конечно,
Гпава 4. Интерфейс графических устройств 97 если вы собираетесь конкурировать с текстовым процессором Microsoft Word for Windows 6.0, то без знания всех деталей управле- ния шрифтами, в том числе технологии TrueType, вам не обойтись. Мы же ограничимся рассмотрением лишь самых необходимых и часто употребляемых функций. По умолчанию в контексте устройства установлен шрифт, назы- ваемый системным. Когда в предыдущих примерах мы выводили текст, то использовали именно его. Шрифт, установленный в текущий момент в контексте устройства, можно заменить другим. Давайте создадим такой шрифт, а затем вос- пользуемся им для вывода текста в рабочую область окна. При создании шрифта необходимо задать его общие атрибуты: высоту и ширину букв; угол оси, по которой выводится текст; чет- кость (толщину) шрифта; вид шрифта (курсивный, подчеркнутый, перечеркнутый, пропорциональный); семейство шрифта и др. Созда- вая шрифт, Windows анализирует заданные пользователем характери- стики и подбирает наиболее подходящий шрифт из числа доступных. Список доступных шрифтов может изменяться с помощью утилиты Fonts, запускаемой из программы Control Panel. Шрифты, устанавли- ваемые утилитой Fonts, кодируют особым образом начертание каж- дого символа кодовой таблицы и должны создаваться специальной программой - редактором шрифтов. Создать шрифт из прикладной программы можно функцией CreateFont или CreateFontlndirect. Они отличаются друг от друга только тем, что CreateFont получает характеристики шрифта как па- раметры функции, a CreateFontlndirect использует для этого логиче- скую запись типа TLogFont. Функция CreateFontlndirect определена в WINPROCS следующим образом: CreateFontIndirect(var LogFont: TLogFont): HFont; где LogFont - запись типа TLogFont, содержащая атрибуты шрифта. Запись TLogFont задает логические параметры шрифта; она опре- делена в модуле WINTYPES следующим образом: TLogFont = record IfHeight: Integer; IfWidth: Integer; IfEscapement: Integer; 0 IfOrientation: Integer; IfWeight: Integer; Ifltalic: Byte; IfUnderline: Byte; IfStrikeOut: Byte; IfCharSet: Byte; IfOutPrecision: Byte; IfClipPrecision: Byte; 4 Зак. 1049
98 Программирование в среде Borland Pascal для Windows IfQuality: Byte; IfPitchAndFamily: Byte; IfFaceName: array [O..lf_FaceSize -1] of Char; end; Поясним назначение полей: • If Height, IfWidth - соответственно высота и ширина шрифта. По- ложительные значения определяют средние размеры ячейки для сим- вола, а отрицательные - средние размеры символа. Нулевое значение IfHeight означает, что шрифт должен иметь стандартный размер. Если нулю равно поле IfWidth, то ширина шрифта выбирается в соответст- вии с коэффициентом сжатия (так, чтобы текст сохранял пропорции и красиво выглядел); • IfEscapement - угол оси, по которой выводится текст. Угол зада- ется в десятых долях градуса, на которые поворачивается текст отно- сительно оси X против хода часовой стрелки; • IfOrientation - поворот каждого символа (в настоящий момент игнорируется); • IfWeight - толщина шрифта. В качестве значений этого поля можно использовать либо значения от 0 до 1000, либо константы fw_Light, fw_Normal, fw_Bold и fwJDontCare, соответствующие тон- кому, нормальному, утолщенному шрифту и толщине по умолчанию; • Ifltalic - ненулевое значение делает шрифт курсивом; • IfUnderline - ненулевое значение делает шрифт подчеркнутым; • IfStrikeOut - ненулевое значение делает шрифт перечеркнутым; • IfCharSet - множество используемых символов: ANSI_CharSet, OEM_CharSet, SymboLCharSet и др.; • If Out Precision - указывает, насколько точно шрифт, предостав- ляемый Windows, должен соответствовать запрошенному. Стандарт- ным значением является Out_Default_Precis; • If Clip Precision - способ усечения частично видимых символов. Стандартным значением является ClipJDefaultJPrecis; • IfQuality - качество шрифта, т.е. насколько точно физический шрифт должен повторять логический. Это поле может принимать значения Default_Quality, Draft_Quality или Proof_Quality; • IfPitchAndFamily - шаг и семейство шрифта. Значение для этого поля формируется на основе поразрядной операции or между кон- стантой шага и константой семейства (табл. 4.3). Шаг шрифта - это расстояние между соседними ячейками символов; он может быть фик- сированным (непропорциональный шрифт) или переменным (пропорциональный шрифт). Семейство - это класс шрифтов со сход- ными характеристиками; • IfFaceName - название шрифта с требуемым начертанием. Если строка IfFaceName - пустая, то начертание определяется другими полями структуры TLogFont.
Гпава 4. Интерфейс графических устройств______________________________99 Табл. 4.3. Константы шага и семейства шрифта Константа Описание Константы шага Fixed_Pitch Фиксированный шаг (непропорциональный шрифт) Variable_Pitch Переменный шаг (пропорциональный шрифт) Default-Pitch Шаг по умолчанию Константы семейства ff_Roman Times New Roman ff_Modem Courier New ff.Script Script fT-Swiss MS Sans Serif ff_Decorative Old English ff_DontCare Название шрифта либо безразлично, либо неизвестно Следующая программа демонстрирует создание шрифта и его ис- пользование при выводе некоторого текста: program FontDemo; uses WinTypes, WinProcs, OWindows, Strings; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) LogFont: TLogFont; { параметры логического шрифта } constructor lnit(ATitle: PChar); procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; PFontDemoApp = ATFontDemoApp; TFontDemoApp = object(TApplication) procedure InitMainWindow; virtual; end; { Конструирует оконный объект и устанавливает параметры шрифта } constructor TMainWindow.lnit(ATitle: PChar); begin inherited lnit(nil, ATitle); with Attr do begin X := 100; Y := 100; W := 450; H := 250; ’ end; with LogFont do begin IfHeight := 50; IfWidth := 0; IfEscapement := 200; IfOrientation := 0;
100 Программирование в среде Borland Pascal для Windows IfWeight := fw_Bold; Ifltalic := 0; IfUnderline := 1; IfStrikeOut := 0; IfCharSet := ANSI_CharSet; IfOutPrecision := Out_Default_Precis; IfClipPrecision := Clip_Default_Precis; IfQuality := Proof_Quality; IfPitchAndFamily := Variable_Pitch or FF_Roman; StrCopy(lfFaceName, Times New Roman'); end; end; { Создает шрифт и использует его для вывода текстовой строки } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); const Message: PChar = 'Borland Pascal'; var NewFont, OldFont: HFont; begin NewFont := CreateFontlndirect(LogFont); OldFont := SelectObject(PaintDC, NewFont); TextOut(PaintDC, 60, 130, Message, StrLen(Message)); SelectObject(PaintDC, OldFont); DeleteObject(NewFont); end; { Создает объект главного окна программы } procedure TFontDemoApp.lnitMainWindow; begin MainWindow := New(PMainWindow, InitfFont Demonstration')); end; var FontDemoApp: TFontDemoApp; begin FontDemoApp.lnit('FontDemo'); FontDemoApp.Run; FontDemoApp. Done; end. Единственное, что делает эта программа, - выводит шрифтом Times New Roman текстовую строку ’Borland Pascal’. Чтобы организо- вать вывод, в объекте главного окна программы потребовалось опре- делить запись LogFont типа TLogFont. Она инициализируется в кон- структоре окна и содержит атрибуты шрифта для вывода текста. Ме- тод Paint использует эту запись и выполняет следующие действия: • создает шрифт на основе логических параметров, заданных в LogFont, и устанавливает его в контексте дисплея; • выводит в окно строку текста ’Borland Pascal’; • восстанавливает в контексте дисплея прежний шрифт и удаляет созданный.
Гпава 4. Интерфейс графических устройств 101 Изменяя значения полей записи LogFont, вы можете подобрать шрифт, который вам понравится, и скопировать значения записи LogFont в свою собственную программу. 4.7. РИСОВАНИЕ ПЕРОМ Перо - это графический инструмент для рисования линий и контуров различных геометрических фигур (прямоугольников, эллипсов и т.д.). Перо характеризуется стилем, толщиной и цветом. Чтобы изменить стиль, толщину или цвет рисуемых линий про- грамма должна создать новое перо и установить его в контексте дис- плея. В дальнейшем это перо будет использоваться при рисовании всех линий и контуров фигур. Новое перо создает функция CreatePen(PenStyle, Width: Integer; Color: TColorRef): HPen; где PenStyle - стиль пера (табл. 4.4); Width - ширина пера в пикселах или логических единицах (если этот параметр равен нулю, то ширина пера устанавливается равной одному пикселу); Color - цвет пера. Дескриптор созданного пера возвращается как значение функции. Табл. 4.4. Стили перьев Константа Описание ps_Solid Рисование сплошной линией ps_Dash Рисование простой штриховой линией (доступно только тогда, когда ширина пера равна 0) ps_Dot Рисование пунктирной линией (доступно только тогда, когда ширина пера равна 0) ps_DashDot Рисование штрихпунктирной линией (доступно только тогда, когда ширина пера равна 0) ps_DashDotDot Рисование штрихдвухпунктирной линией (доступно только тогда, когда ширина пера равна 0) ps_Null Рисование невидимой линией ps_InsideFrame Рисование границ замкнутых фигур. Использование этой установки гарантирует, что внешние границы фигуры не превысят заданных, даже если ширина линии больше 1 * Существует еще одна функция создания пера - CreatePenlndirect, которая в некоторых случаях бывает более удобной: CreatePenIndirect(var LogPen: TLogPen): HPen; где LogPen - запись типа TLogPen с информацией о стиле, ширине и
102 Программирование в среде Borland Pascal для Windows цвете создаваемого пера. Тип данных TLogPen объявлен так: TLogPen = record lopnStyle: Word; lopnWidth: TPoint; lopnColor: Longint; end; где поля lopnStyle, lopnWidth и lopnColor задают соответственно стиль, ширину и цвет пера. В поле lopnWidth используется только компонен- та X записи TPoint. Чтобы задействовать созданное функцией CreatePen или CreatePenIndirect перо, его следует установить в контексте устройства вызовом SelectObject. Программа PENS наглядно демонстрирует этот процесс с различными стилями перьев: program Pens; uses WinTypes, WinProcs, OWindows; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; TPensApp = object(TApplication) procedure InitMainWindow; virtual; end; { Рисует линии различными стилями } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var ClientRect: Trect; { Рисует линию заданным стилем и пишет название стиля } procedure DrawLine(lndex: Integer; PenStyle: Integer; StyleName: String); var NewPen, OldPen: HPen; YPos: Integer; begin YPos := ClientRect.Top + Index * ClientRect.Bottom div 6; SetTextAlign(PaintDC, ta_Bottom); TextOut(PaintDC, 8, YPos, @StyleName[1], Length(StyleName)); lnc(YPos, 2); NewPen := CreatePen(PenStyle, 1, RGB(0, 0, 128)); OldPen := SelectObject(PaintDC, NewPen); MoveTo(PaintDC, 4, YPos); LineTo(PaintDC, ClientRect.Right - 4, YPos); SelectObject(PaintDC, OldPen); DeleteObject(NewPen); end; begin GetClientRect(HWindow, ClientRect);
Глава 4. Интерфейс графических устройств 103 DrawLine(1, ps_Solid, ,ps_Solid'); DrawLine(2, ps_Dash, 'ps_Dash'); DrawLine(3, ps_Dot, 'ps_Dot'); DrawLine(4, ps_DashDot, 'ps_DashDot'); DrawLine(5, ps_DashDotDot, 'ps_DashDotDot'); end; { Создает объект главного окна программы } procedure TPensApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'Pen Styles')); end; var PensApp: TPensApp; begin PensApp. I nit('Pens'); PensApp.Run; PensApp. Done; end. Метод Paint главного окна программы выполняет последователь- ный вызов локальной процедуры DrawLine для рисования линии каж- дого стиля. Процедура DrawLine сначала выводит имя стиля с помо- щью функции TextOut, а затем создает перо требуемого стиля и рисует им горизонтальную линию. 4.8. РИСОВАНИЕ КИСТЬЮ Кисть - это инструмент для “закрашивания” различных областей изображения. Функции рисования прямоугольников, эллипсов, мно- гоугольников и других замкнутых фигур используют текущую кисть контекста устройства для заполнения их внутреннего пространства. Закрасить область можно по-разному: • залить сплошным цветом; • заштриховать одной из стандартных штриховок; • заполнить многократным повторением небольшой картинки. Во всех этих случаях кисть физически представляет собой некото- рый битовый образ, многократное повторение которого на заданной площади с отсечением лишних частей приводит к заполнению. Каждому из перечисленных видов заполнений соответствует опре- деленный тип кисти. Создать новую кисть для заливки можно с по- мощью функции CreateSolidBrush(Color: TColorRef): HBrush; где Color - цвет кисти. Дескриптор кисти возвращается как значение функции. По умолчанию в контексте устройства установлена сплош- ная кисть белого цвета.
104 Программирование в среде Borland Pascal для Windows Для штриховки области кисть нужно создавать функцией CreateHatchBrush(Index: Integer; Color: TColorRef): HBrush; где Index - стиль (вид) штриховки (табл. 4.5); Color - цвет линий штриховки. Дескриптор кисти возвращается как значение функции. Табл. 4.5. Виды штриховок Константа Описание hs_Horizontal Штриховка тонкими горизонтальными линиями hs_Vertical Штриховка тонкими вертикальными линиями hs.BDiagonal Штриховка тонкими параллельными линиями под углом 45 градусов относительно оси X hs_FDiagonal Штриховка тонкими параллельными линиями под углом -45 градусов относительно оси X hs_Cross Совмещение штриховок hs_Horizontal и hs_Vertical hs.DiagCross Совмещение штриховок hs_BDiagonal и hs_FDiagonal Для заполнения области битовым образом (он заранее подготав- ливается самим пользователем) кисть создается с помощью функции CreatePatternBrush(Bitmap: HBitmap): HBrush; где Bitmap - дескриптор растрового изображения, из которого созда- ется кисть. Растровое изображение может быть создано с помощью функции CreateBitmap или загружено из ресурса приложения вызовом функции LoadBitmap (растровые изображения описаны в гл. 5). Отме- тим, что созданная с помощью функции CreatePatternBrush кисть ис- пользует только левую верхнюю часть растрового изображения вели- чиной 8x8 точек. Кроме описанных выше, существует еще функция CreateBrushlndirect, которая позволяет создать кисть любого типа на основе записи TLogBrush: CreateBrushIndirect(var LogBrush: TLogBrush): HBrush; где LogBrush - переменная типа TLogBrush, содержащая параметры создаваемой кисти. Запись TLogBrush имеет следующий формат: TLogBrush = record IbStyle: Word; IbColor: Longint; IbHatch: Integer; end; где поля IbStyle, IbColor и IbHatch задают соответственно стиль, цвет и штриховку кисти. Возможные значения поля IbStyle приведены в
Глава 4. Интерфейс графических устройств________________________105 табл. 4.6. Интерпретация полей IbColor и IbHatch зависит от типа кисти, т.е. от значения поля IbStyle. Например, если IbStyle = bs_Solid (сплошная кисть), то IbColor содержит цвет заполнителя, а поле IbHatch вообще игнорируется. Если же IbStyle = bs_Hatched (штриховка), то IbColor задает цвет линий штриховки, a IbHatch - ее стиль (см. табл. 4.5). В том случае, когда заполнителем кисти является растровое изображение, его дескриптор помещается в поле IbHatch. Более подробно создание кисти на основе записи TLogBrush рассмот- рено в справочнике по Borland Pascal. Табл. 4.6. Стили кисти Константа Описание bs_Solid bs_Null (bs_Hollow) bs_Hatched bS-Patterp bs_DIBPattem Сплошная кисть Невидимая кисть Кисть с одной из стандартных штриховок Заполнитель кисти - растровое изображение Заполнитель кисти — аппаратно-независимое растровое изображение После работы приложение должно удалить все созданные кисти с помощью функции DeleteObject. Если кисть создавалась на основе растрового изображения, то его следует удалять отдельно. Работу с кистью покажем на примере следующей программы: program Brushes; { Подключение файла ресурса с растровым изображением} {$R BRUSHES.RES} uses WinTypes, WinProcs, OWindows; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; TBrushesApp = object(TApplication) procedure InitMainWindow; virtual; end; {Рисует в окне прямоугольники с различными видами заполнения} procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var ClientRect: TRect; Rect: TRect; { Рассчитывает координаты прямоугольника в окне } procedure CalcRectBounds(Xlndex, Ylndex: Integer; var R: TRect); var XSize, YSize: Integer; begin
106 Программирование в среде Borland Pascal для Windows XSize := ClientRect.Right div 3; YSize := ClientRect. Bottom div 3; R.Left := 0; R.Top := 10; R.Right := XSize; R. Bottom := YSize; lnflateRect(R, -XSize div 5, -YSize div 5); OffsetRect(R, XSize * (Xlndex -1), YSize * (Ylndex -1)); end; { Рисует прямоугольник кистью, установленной по умолчанию } procedure DrawDefaultRect; var NewBrush, OldBrush: HBrush; begin SetTextAlign(PaintDC, ta_Bottom); TextOut(PaintDC, Rect.Left, Rect.Top-4, 'Default', 7); Rectangle(PaintDC, Rect.Left, Rect.Top, Rect.Right, Rect.Bottom)* end; { Рисует закрашенный прямоугольник ярко-синего цвета } procedure DrawSolidRect; var NewBrush, OldBrush: HBrush; begin SetTextAlign(PaintDC, ta__Bottom); TextOut(PaintDC, Rect.Left, Rect.Top - 4, 'Solid', 5); NewBrush := CreateSolidBrush(RGB(0, 0, 255)); OldBrush := SelectObject(PaintDC, NewBrush); Rectangle(PaintDC, Rect.Left, RectTop, Rect.Right, Rect.Bottom); SelectObject(PaintDC, OldBrush); DeleteObject(NewBrush); end; { Рисует прямоугольник, заштрихованный заданным способом } procedure DrawHatchedRect(HatchStyle: Integer; StyleName: String); var NewBrush, OldBrush: HBrush; begin SetTextAlign(PaintDC, ta_Bottom); TextOut(PaintDC, Rect.Left, Rect.Top - 4, @StyleName[1], Length(StyleName)); NewBrush := CreateHatchBrush(HatchStyle, RGB(0, 0, 255)); OldBrush := SelectObject(PaintDC, NewBrush); Rectangle(PaintDC, RectLeft, Rect.Top, Rect.Right, Rect.Bottom); SelectObject(PaintDC, OldBrush); DeleteObject(NewBrush); end; { Рисует прямоугольник, заполненный растровым изображением } procedure DrawPatternRect; var NewBrush, OldBrush: HBrush; Bitmap: HBitmap;
Гпава 4. Интерфейс графических устройств 107 begin SetTextAlign(PaintDC, ta_Bottom); TextOut(PaintDC, Rect.Left, Rect.Top - 4, 'Pattern', 7); Bitmap := LoadBitmap(Hlnstance, MakelntResource(IOO)); NewBrush := CreatePatternBrush(Bitmap); DeleteObject(Bitmap); OldBrush := SelectObject(PaintDC, NewBrush); Rectangle(PaintDC, Rect.Left, Rect.Top, Rect.Right, Rect.Bottom); SelectObject(PaintDC, OldBrush); DeleteObject(NewBrush); end; begin GetClientRect(HWindow, ClientRect); CalcRectBounds(1,1, Rect); DrawDefaultRect; CalcRectBounds(1, 2, Rect); DrawSolidRect; CalcRectBounds(1, 3, Rect); DrawPatternRect; CalcRectBounds(2,1, Rect); DrawHatchedRect(hs_Horizontal, ’hs_Horizontal'); CalcRectBounds(2, 2, Rect); DrawHatchedRect(hs_Vertical, 'hs_Vertical'); CalcRectBounds(2, 3, Rect); DrawHatchedRect(hs_BDiagonal, 'hs_BDiagonal'); CalcRectBounds(3, 1, Rect); DrawHatchedRect(hs_FDiagonal, 'hs_FDiagonal'); CalcRectBounds(3, 2, Rect); DrawHatchedRect(hs_Cross, 'hs_Cross'); CalcRectBounds(3, 3, Rect); DrawHatchedRect(hs_DiagCross, 'hs_DiagCross'); end; { Создает объект главного окна программы } procedure TBrushesApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'Brush Styles')); end; var BrushesApp: TBrushesApp; begin BrushesApp.lnit('Brushes'); BrushesApp.Run; BrushesApp. Done; end. Программа BRUSHES рисует в главном окне приложения различ- ным образом заполненные прямоугольники. Сохранение пропорций изображения при изменении размеров окна обеспечивает локальная процедура CalcRectBounds метода Paint, вызываемая перед отображе- нием каждого прямоугольника. Она вычисляет координаты прямо- угольника в соответствии с его относительным положением. Проце- дура DrawDefaultRect рисует прямоугольник, используя для его за- полнения кисть, установленную в контексте дисплея по умолчанию. Процедуры DrawSolidRect, DrawPatternRect и DrawHatchedRect от- личаются тем, что используют для заполнения прямоугольников кис- ти, создаваемые соответственно функциями CreateSolidBrush, CreatePatternBrush и CreateHatchBrush.
108 Программирование в среде Borland Pascal для Windows 4.9. РЕЖИМЫ ОТОБРАЖЕНИЯ До сих пор графический вывод в рассматриваемых нами программах осуществлялся в единицах устройства, т.е. в пикселах. Систему коор- динат, единицами измерения в которой являются пикселы, принято называть физической (device coordinates). Физическая система коорди- нат характеризуется двумя перпендикулярными осями X и Y. Ось X является горизонтальной, направленной вправо, а ось Y - вертикаль- ной, направленной вниз. Так как координаты кодируются целыми числами, их значения могут изменяться в диапазоне от -32768 до 32767 по каждой из осей. При разработке большинства приложений вполне достаточно фи- зической координатной системы, которую Windows предоставляет по умолчанию. Однако в некоторых ситуациях пользоваться такой сис- темой координат становится крайне затруднительно, например, когда изображение должно иметь точные размеры и пропорции. В этом случае нужна логическая система координат. Универсальная графиче- ская программа, способная выполняться в различных графических средах, скорее всего будет использовать именно логическую систему координат (logical coordinates). Так же, как и физическая, логическая система координат характе- ризуется двумя перпендикулярными осями: горизонтальной осью X и вертикальной осью Y. Диапазон значений координат от -32 768 до 32 767. Однако если физические координаты устройства отсчитыва- ются в пикселах, то в логической системе единицами измерения могут быть и пикселы, и миллиметры, и дюймы. Кроме того, в логической координатной системе направление осей X и Y можно задать. Одна из проблем универсальных графических интерфейсов состоит в согласовании логического и физического графических экранов. В GDI пересчет логических координат (от англ. Logical Points - LP) в физические (от англ. Device Points - DP) состоит из трех шагов: • параллельный перенос изображения на логической плоскости пу- тем вычитания из координат каждой точки изображения заданных константных значений. Смещения задаются отдельно по каждому из направлений X и Y; они могут быть положительными или отрица- тельными; • масштабирование (сжатие или растяжение) полученного изобра- жения путем умножения координат каждой его точки на заданные масштабные коэффициенты. Масштабные коэффициенты задаются отдельно по каждому из направлений X и Y; они могут быть положи- тельными или отрицательными. В результате масштабирования изо- бражение переводится на физическую плоскость; • параллельный перенос изображения на физической плоскости путем добавления к координатам каждой физической точки изобра-
Гпава 4. Интерфейс графических устройств 109 жения заданных константных значений. Смещения задаются отдельно по каждому из направлений Хи Y; они могут быть положительными или отрицательными. Преобразование физических координат в логические выполняется в обратном порядке. Для пересчета одних координат в другие исполь- зуются следующие несложные формулы: Пересчет логических координат в физические Dx Смещение по оси X в логической системе координат Масштабный коэффициент по оси X Смещение по оси X в физической системе координат = (Lx - xWO) * xVE/xWE + xVO Dy = (Ly - yWO) * у VE / у WE + yVO Смещение по оси Y в логической системе координат Масштабный коэффициент по оси Y Смещение по оси Y в физической системе координат Пересчет физических координат в логические Смещение по оси X в физической системе координат Обратный масштабный коэффициент по оси X Смещение по оси X в логической системе координат Lx Ly = (Dx - xVO) * = (Dy - yVO) * xWE/xVE yWE/yVE + xWO + yWO Обратный масштабный коэффициент по оси Y Смещение по оси Y в физической системе координат Смещение по оси Y в логической системе координат Параметры в формулах имеют следующий смысл: Lx, Ly - коорди- наты точки на логической координатной плоскости; Dx, Dy - коорди- наты точки на физической координатной плоскости; xWO, у WO - смещения по осям X и Y на логической плоскости; xVO,yVO - смеще- ния по осям X и Y на физической плоскости; xVE/xWE, yVE/yWE - масштабные коэффициенты, задающие необходимое сжатие или рас- тяжение изображения по осям X и Y; xWE/xVE, yWE/yVE - обратные масштабные коэффициенты.
по Программирование в среде Borland Pascal для Windows Если абсолютное значение xVE больше абсолютного значения xWE (Abs(xVEZxWE) > 1), то изображение в пикселах растягивается по оси X. Если же абсолютное значение xVE меньше абсолютного значе- ния xWE (Abs(xVEZxWE) < 1), то изображение в пикселах сжимается по оси X. Знак отношения xVEZxWE определяет направление оси X логической системы координат. Если знак положительный, то ось X направлена вправо, а если отрицательный, - то влево. Подобные утверждения справедливы и для величин yVE и yWE, а именно: если абсолютное значение yVE больше абсолютного значения yWE (Abs(yVEZyWE) > 1), то изображение в пикселах растягивается по оси Y; если же абсолютное значение yVE меньше абсолютного значе- ния yWE (Abs(yVEZyWE) < 1), то изображение в пикселах сжимается по оси У. Знак отношения yVEZyWE определяет направление оси У логической системы координат. Если знак положительный, то ось У направлена вниз, а если отрицательный, - то вверх. Для преобразования логических координат в физические и обрат- но GDI использует функции: LPtoDP(DC: HDC; var Points; Count: Integer): Bool; DPtoLP(DC: HDC; var Points; Count: Integer): Bool; где DC - контекст устройства; Points - массив точек (каждая имеет тип данных TPoint); Count - количество точек в массиве Points. Функ- ция LPtoDP транслирует логические координаты в физические, а функция DPtoLP - наоборот физические координаты в логические. Массив исходных точек передается в параметре Points. Сюда же по- мещается и результат. Нетрудно догадаться, что большинство функций GDI, выполняю- щих вывод того или иного графического примитива, сначала всегда обращаются к функции LPtoDP для преобразования переданных им логических координат графического примитива в координаты уст- ройства и уже в них осуществляют рисование. С целью геометрической интерпретации параметров xWO, yWO, xVO, yVO, xWE, yWE, xVE, yVE в GDI были введены понятия услов- ного окна проекции и условного логического окна (рис. 4.3). Под ус- ловным окном проекции (viewport) понимается прямоугольная область экрана, которой соответствует некоторая часть логической коорди- натной системы. Эта часть логической системы координат известна как условное логическое окно (logical window). Параметры (xVO, yVO) задают начальное положение окна проекции (Viewport Origin), пара- метры (xWO, yWO) - начальное положение логического окна (Window Origin); параметры (xVE, yVE) определяют размеры окна проекции (Viewport Extents), а параметры (xWE, yWE) - размеры ло- гического окна (Window Extents). Обратите особое внимание на то,
Гпава 4. Интерфейс графических устройств 111 Рис. 4.3. Отображение логических координат в физические что отдельные размеры логического окна и окна проекции лишены смысла, важны только их взаимные отношения! Допустим, нам необходимо переместить начало координат в центр рабочей области окна. Для этого можно воспользоваться функцией SetViewportOrg (или улучшенной SetViewportOrgEx), устанавливаю- щей физическое начало координат. Выполняемые при этом действия выглядят так: GetClientRect(HWindow, R); SetViewportOrg(PaintDC, R.Right div 2, R.Bottom div 2); Такого же результата можно достичь переустановкой логического начала координат с помощью функции SetWindowOrg (или улучшен- ной SetWindowOrgEx). Однако действия, которые при этом нужно выполнить, выглядят несколько сложнее: $ GetClientRect(HWindow, R); DPtoLP(PaintDC, R, 2); SetWindowOrg(PaintDC, -R.Right div 2, -R.Bottom div 2); Эти способы различаются тем, что для функции SetViewportOrg параметры задаются в пикселах, а для SetWindowOrg - в логических единицах. Поэтому в последнем случае дополнительно вызывается
112 Программирование в среде Borland Pascal для Windows функция DPtoLP для преобразования физических координат рабочей области в логические координаты. Следует отметить, что если контекст устройства, в который на- правляется графический вывод, связан с окном, то действительные координаты изображения относительно левого верхнего угла экрана зависят, помимо всего прочего, еще и от положения окна на экране. Для масштабирования изображения, т.е. изменения в контексте устройства масштабных коэффициентов, служат функции SetViewportExt и SetWindowExt (и их улучшенные аналоги SetViewportExtEx и SetWindowExtEx). Функция SetViewportExt изме- няет размеры условного окна проекции, а функция SetWindowExt - размеры условного логического окна. В приведенном ниже про- граммном фрагменте размеры условного логического окна устанав- ливаются равными 3 по оси X и 5 по оси У, а размеры условного окна проекции получают значения 2 по оси X и -4 по оси У: SetWindowExt (DC, 3, 5); SetViewportExt (DC, 2, -4); В результате задаются следующие преобразования: по оси X - трех логических точек в две физические, а по оси У - пяти логических то- чек в четыре физические. Положительные значения логических X- координат преобразуются в положительные значения физических X- координат, а положительные значения логических Y-координат пре- образуются в отрицательные значения физических У-координат. Масштабные коэффициенты по оси X и по оси У равны соответствен- но 2/3 и -4/5. Значения масштабных коэффициентов регламентируются еще од- ним атрибутом контекста устройства - режимом масштабирования (mapping mode). Режимы масштабирования позволяют программисту не заботиться о значениях масштабных коэффициентов, поскольку GDI автоматически задает им правильные значения вместе с установ- кой режима масштабирования. GDI обеспечивает восемь режимов масштабирования, которые перечислены в табл. 4.7. Чтобы установить в контексте устройства новый режим масшта- бирования, нужно вызвать функцию SetMapMode(DC: HDC; MapMode: Integer): Integer; где DC - контекст устройства; MapMode - устанавливаемый режим масштабирования. Функция возвращает режим, который был уста- новлен ранее. С помощью функции GetMapMode(DC: HDC): Integer; можно узнать текущий режим масштабирования.
Гпава 4. Интерфейс графических устройств ИЗ Табл. 4.7. Режимы масштабирования Режим Логические единицы Физические единицы Направление осей X Y mm_Text 1 1 пиксел Вправо Вниз mm_Lo Metric 10 1 миллиметр Вправо Вверх mm_HiMetric 100 1 миллиметр Вправо Вверх mm_LoEnglish 100 1 дюйм Вправо Вверх mm_HiEnglish 1000 1 дюйм Вправо Вверх mm_Twips 1440 1 дюйм Вправо Вверх mm_Isotropic Задается Задается Вправо Вверх mm_Anisotropic Задается Задается Вправо Вверх По умолчанию GDI предоставляет режим mm_Text. В этом режиме одна логическая единица соответствует одной единице устройства - пикселу. Положительные значения координат отсчитываются по оси X вправо, а по оси У вниз. Слово Text в названии режима указывает на нормальный способ вывода текста - слева направо с продолжени- ем вниз. В режиме mm_Text невозможна переориентация осей коорди- нат и масштабирование изображения (размеры окна проекции и раз- меры логического окна совпадают и равны 1 по каждому из двух на- правлений). Этот режим автоматически использовался во всех преды- дущих примерах программ. Режимы mm_LoMetric, mm_HiMetric, mm_LoEnglish, mm_HiEnglish и mm_Twips позволяют программе создавать изобра- жение в единицах измерения, используемых в физике. В режиме mm_LoMetric 10 логических единиц соответствуют одному миллимет- ру. В режиме mm_HiMetric 100 логических единиц равны одному мил- лиметру. В режиме mm_LoEnglish 100 логических единиц соответст- вуют одному дюйму. Если установлен режим mm_HiEnglish, то одно- му дюйму соответствует 1000 логических единиц. Режим mm_Twips позволяет создавать изображение в пунктах (один пункт равен 1/1440 дюйма); он устанавливает масштабирование, при котором 1440 еди- ниц логической поверхности равны одному дюйму физической по- верхности. Последний режим особенно удобен при работе со шриф- тами и выводе текста. Все пять режимов масштабирования имеют ось X, направленную вправо, и ось У - вверх, как и в декартовой системе координат. Это отличает их от режима mm_Text, в котором ось У направлена вниз. По той причине, что результирующее изображение всегда получает точные физические размеры, выраженные в тех или иных единицах измерения, в рассмотренных режимах изменить мас- штабные коэффициенты невозможно. Невозможно также изменить ориентацию осей координат. Вследствие этого все шесть режимов
114 Программирование в среде Borland Pascal для Windows mm_Text, mm_LoMetric, mm_HiMetric, mm_LoEnglish, mm_HiEnglish и mm_Twips называют принудительными (constrained). Режим mm_Anisotropic является Наиболее универсальным и гиб- ким; при некотором усилии он позволяет сымитировать работу любо- го из остальных режимов. По этой же причине он наименее удобен, так как возлагает на программиста все заботы по соблюдению про- порций изображения. В данном режиме можно устанавливать любые размеры условного окна проекции и условного логического окна с помощью функций SetViewportExt и SetWindowExt. Режим mm_Isotropic используется в том случае, когда создаваемые графические фигуры должны иметь точную форму. Дело в том, что для определенного вида адаптеров (например, EGA) пикселы экрана имеют не круглую, а эллиптическую форму. При этом высота пиксела обычно превышает его ширину. Отношение ширины пиксела к высоте известно как коэффициент пропорциональности пиксела (aspect ratio). В режиме mm_Isotropic коэффициент пропорциональности учитыва- ется при выводе графической информации с целью коррекции изо- бражения. Это делается за счет соответствующей установки размеров логического окна и окна проекции. Кстати сказать, коррекция выпол- няется и в режимах mm_LoMetric, mm_HiMetric, mm_LoEnglish, mrn_HiEnglish и mm_Twips. Изначально масштабные коэффициенты в режиме mm_Isotropic совпадают с масштабными коэффициентами в режиме mm_LoMetric, но они могут быть впоследствии изменены (с помощью функций SetViewportExt и SetWindowExt). По этой причине режим mm_Isotropic иногда называют частично принудительным. Использование режимов масштабирования показано в программе MAPMODES, которая выводит в окно график функции синус. Как известно, эта функция является периодической с периодом, равным 2*Pi. Вывод графика осуществляется таким образом, чтобы в окне приложения умещалось ровно два периода, а амплитуда была равна четверти высоты окна. При изменении ширины или высоты окна про- порционально изменяется и график функции. Этот эффект достигается правильной установкой условного окна проекции и условного логи- ческого окна в режиме масштабирования mm_Anisotropic. Для построения графика функции синус используется ее кусочно- линейная аппроксимация. Функция вычисляется для ограниченного числа значений аргумента X, лежащих в диапазоне от 0 до 2 * Pi. От- счеты по оси X выбираются равноотстоящими друг от друга, причем чем их больше, тем точнее аппроксимация. Полученные точки соеди- няются между собой отрезками прямых линий, образуя синусоиду. Так как функция синус является периодической, достаточно рассчи- тать ее значения и построить график лишь на одном периоде, на всех остальных периодах он будет иметь точно такой же вид. (Вообще говоря, чтобы построить график функции синус, достаточно рассчи-
Гпава 4. Интерфейс графических устройств 115 тать значения только на одной четверти периода.) При расчетах ис- пользуется следующая формула: У = Round( А * sin(X * 2 * Pi / N)), где N - число отсчетов на периоде; X - значение аргумента на интер- вале от 0 до N; А - целочисленная амплитуда; Y - значение функции на интервале от -А до А. В программе N = 1024 и А = 512. Вот ее исходный текст: program MapModes; uses WinTypes, WinProcs, OWindows; const Period = 1024; { период синуса } Amplitude =512; { амплитуда синуса } type PPointsArray = ATPointsArray; TPointsArray = array [0..Period] of TPoint; PMainWindow = ATMainWindow; TMainWindow = object(TWindow) Points: PPointsArray; { массив значений синуса на одном периоде } constructor lnit(AParent: PWindowsObject; ATitle: PChar); destructor Done; virtual; procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; PMapModesApp = ATMapModesApp; TMapModesApp = object(TApplication) procedure InitMainWindow; virtual; end; { Рассчитывает точки графика, помещая их в динамический массив Points } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); var I: Integer; begin inherited lnit(AParent, ATitle); New(Points); for I := 0 to Period do begin PointsA[l].X := I; PointsA[l].Y := Round(Amplitude * Sin(l * Pi * 2 / Period)); end; end; $ { Уничтожает динамический массив Points, содержащий точки графика } destructor TMainWindow.Done; begin if Points <> nil then Dispose(Points); inherited Done; end; { Рисует в окне график функции синус } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct);
116 Программирование в среде Borland Pascal для Windows var ClientRect: TRect; HalfWidth, HalfHeight: Integer; Pen, OldPen: HPen; { Рисует оси координат } procedure DrawAxes; var R: TRect; begin SetViewportOrg(PaintDC, HalfWidth, HalfHeight); MoveTo(PaintDC, -HalfWidth -1,0); LineTo(PaintDC, HalfWidth + 1,0); R.Left := 3; R.Top := 3; R.Right := HalfWidth - 6; R.Bottom := HalfHeight - 6; DrawText(PaintDC, ’X’, -1, R, dt_Top or dt Right); MoveTo(PaintDC, 0, -HalfHeight -1); LineTo(PaintDC, 0, HalfHeight + 1); OffsetRect(R, 0, -HalfHeight); DrawText(PaintDC, ’Y', -1, R, dt_Top or dt_Left); end; begin GetClientRect(HWindow, ClientRect); HalfWidth := ClientRect. Right div 2; HalfHeight := ClientRect.Bottom div 2; DrawAxes; { Устанавливаем режим отображения и масштабные коэффициенты } SetMapMode(PaintDC, mm_Anisotropic); SetViewportExt(PaintDC, HalfWidth, -HalfHeight); SetWindowExt(PaintDC, Period, Period); { Создаем тонкое красное перо и выбираем его в контексте дисплея } Pen := CreatePen(ps_Solid, 0, RGB(192, 0, 0)); OldPen := SelectObject(PaintDC, Pen); { Выводим левую часть графика на интервале [-Period, 0]} SetViewportOrg(PaintDC, 0, HalfHeight); Polyline(PaintDC, Points'*, Period + 1); { Выводим правую часть графика на интервале [0, Period]} SetViewportOrg(PaintDC, HalfWidth, HalfHeight); Polyline(PaintDC, Points'*, Period + 1); { Восстанавливаем прежнее перо и удаляем использованное } SelectObject(PaintDC, OldPen); DeleteObject(Pen); end; { Создает объект главного окна программы } procedure TMapModesApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’y = sin(x)’)); end;
Гпава 4. Интерфейс графических устройств 117 var MapModesApp: TMapModesApp; begin MapModesApp. InitfMapModes'); MapModesApp. Run; MapModesApp. Done; end. Расчет точек графика осуществляется один раз в конструкторе объекта главного окна. Все точки помещаются в массив, адресуемый указателем Points. Вывод графика в окно приложения происходит в методе Paint. Вложенная процедура DrawAxes обеспечивает рисование осей коор- динат. После ее вызова метод Paint устанавливает в контексте ото- бражения PaintDC режим масштабирования mm_Anisotropic. Соответствующее сжатие или растяжение графика, о котором шла речь ранее, обеспечивается с помощью функций SetViewportExt и SetWindowExt. Размеры условного окна проекции устанавливаются равными половине ширины и половине высоты рабочей области ок- на, Размеры условного логического окна устанавливаются равными 1024x1024. График синуса рисуется тонким красным пером. Сначала выводит- ся один период (левая часть графика), а потом второй (правая часть графика). Для вывода каждого периода используется GDI-функция Polyline, которой в обоих случаях передаются одни и те же параметры. Чтобы часть синусоиды появилась в нужном месте, перед вызовом Polyline происходит обращение к функции SetViewportOrg для соот- ветствующей установки начала координат. 4.10. РИСОВАНИЕ С ПОМОЩЬЮ МЫШИ В данном параграфе мы рассмотрим механизм рисования с помощью мыши. Для начала разработаем простую программу, которая позво- ляет рисовать в окне произвольные кривые. Она должна обладать описанными ниже свойствами. Если во время нахождения курсора в пределах главного окна про- граммы нажать левую кнопку мыши, и, удерживая ее, начать передви- гать мышь, то курсор будет оставлять в окне непрерывный след. При отпускании кнопки мыши рисование прекратится. Пользователь мо- жет повторять данный процесс любое количество раз. Очистка окна производится по двухкратному нажатию левой кнопки мыши в его рабочей области. Для того чтобы создать программу с указанными свойствами, не- обходимо решить два основных вопроса: организовать хранение ин- формации о нарисованных в окне кривых и заставить курсор мыши
118 Программирование в среде Borland Pascal для Windows оставлять след в окне. Следующая программа демонстрирует, как это сделать: program MouseDraw; uses WinTypes, WinProcs {$ifdefVer80}, Messages {$endif}, OWindows, Objects; type PPointsCollection = ATPointsCollection; TPointsCollection = object(TCollection) procedure Freeltem(ltem: Pointer); virtual; end; PMainWindow = ATMainWindow; TMainWindow = object(TWindow) ButtonDown: Boolean; {кнопка мыши нажата?} Hold DC: HDC; { вспомогательный контекст дисплея } Points: PPointsCollection; {здесь накапливаются точки кривых } constructor lnit(AParent: PWindowsObject; ATitle: PChar); destructor Done; virtual; function GetClassName: PChar; virtual; procedure GetWindowClass(var AWndClass: TWndClass); virtual; procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; procedure WMLButtonDown(var Msg: TMessage); virtual wm_First + wm_L Button Down; procedure WMLButtonUp(var Msg: TMessage); virtual wm_First + wm_LButtonllp; procedure WMMouseMove(var Msg: TMessage); virtual wm_First + wm_MouseMove; procedure WMLButtonDblClk(var Msg: TMessage); virtual wm_First + wm_LButtonDblClk; end; PMouseDrawApp = ATMouseDrawApp; TMouseDrawApp = object(TApplication) procedure InitMainWindow; virtual; end; const NilPoint: TPoint = (X: -32767; Y: -32767); {"пустая” точка } { Освобождает элемент коллекции } procedure TPointsCollection.Freeltem(ltem: Pointer); begin Dispose(PPoint(ltem)); end; { Конструирует оконный объект и создает коллекцию точек} constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); ButtonDown := False; New(Points, lnit(100, 20)); end; { Уничтожает коллекцию точек и разрушает оконный объект } destructor TMainWindow. Done; begin
Гпава 4. Интерфейс графических устройств 119 if Points <> nil then Dispose(Points, Done); inherited Done; end; {Возвращает имя нового оконного класса} function TMainWindow.GetClassName: PChar; begin GetClassName := ’MouseDrawWindow’; end; { Возвращает атрибуты нового оконного класса } procedure TMainWindow.GetWindowClass(var AWndClass: TWndClass); begin inherited GetWindowClass(AWndClass); with AWndClass do Style := Style and not (cs_HRedraw or cs_VRedraw) or cs_DblClks; end; { Рисует в окне линии } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var LineBreak: Boolean; { Рисует отрезок прямой с обнаружением конца линии } procedure DrawLine(Point: PPoint); far; begin if (PointA.X = NilPointX) and (PointA.Y = NilPoint.Y) then LineBreak := True else begin if LineBreak then MoveTo(PaintDC, PointA.X, PointA.Y) else LineTo(PaintDC, PointA.X, PointA.Y); LineBreak := False; end; end; begin LineBreak := True; Points'*. ForEach(@DrawLine); end; { Подготавливает объект к рисованию при нажатии левой кнопки мыши } procedure TMainWindow.WMLButtonDown(var Msg: TMessage); var StartPoint: PPoint; begin if not ButtonDown then begin ButtonDown := True; SetCapture(HWindow); HoldDC := GetDC(HWindow); MoveTo(HoldDC, Msg.LParamLo, Msg.LParamHi); New(StartPoint); with StartPointA, Msg do
120 Программирование в среде Borland Pascal для Windows begin X := LParamLo; Y := LParamHi; end; Points'*. I nsert(StartPoint); end; end; { Завершает рисование при отпускании левой кнопки мыши } procedure TMainWindow.WMLButtonllp(var Msg: TMessage); var EndPoint: PPoint; begin if ButtonDown then begin New(EndPoint); EndPoint7* := NilPoint; Points'*. Insert(EndPoint); ReleaseDC(HWindow, HoldDC); Releasecapture; ButtonDown := False; end; end; { Рисует приращение линии при передвижении мыши } procedure TMainWindow.WMMouseMove(var Msg: TMessage); var NextPoint: PPoint; begin if ButtonDown then begin LineTo(HoldDC, Msg.LParamLo, Msg.LParamHi); New(NextPoint); with NextPoint7*, Msg do begin X := LParamLo; Y := LParamHi; end; PointsA.lnsert(NextPoint); end; end; { Удаляет все кривые и очищает рабочую область окна } procedure TMainWindow.WMLButtonDblClk(var Msg: TMessage); begin Points'*. FreeAII; I nvalidateRect(H Window, nil, True); end; { Создает объект главного окна программы } procedure TMouseDrawApp. InitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Drawing with mouse’)); end;
Гпава 4. Интерфейс графических устройств 121 var MouseDrawApp: TMouseDrawApp; begin MouseDrawApp.lnit(’MouseDrw'); MouseDrawApp. Run; MouseDrawApp. Done; end. Прокомментируем программу MOUSEDRW. Каждая кривая аппроксимируется ломаной линией, т.е. представляется в виде после- довательности точек, соединенных между собой отрезками прямых. Поле Points объекта TMainWindow содержит указатель на объект типа TPointsCollection, который хранит и накапливает эти точки. По- следовательность точек, относящихся к одной и той же непрерывной кривой, завершается “пустой” точкой с координатами X = -32767 и У = -32767. Точка с координатами (-32767, -32767) играет роль разде- лителя среди точек разных кривых. При изменении размеров окна нарисованное с помощью мыши изображение не меняет масштаб. Поэтому метод GetWindowClass объекта TMainWindow убирает в оконном классе стили cs_HRedraw и cs_VRedraw, чтобы исключить бессмысленные перерисовки всего окна при изменении его горизонтального и вертикального размеров. В результате перерисовываются только те области, которые становятся недействительными. Обычно окно никогда не получает сообщения от мыши, находя- щейся за его пределами. Однако для рисования мышью необходимо, чтобы в течение времени, когда нажата левая кнопка мыши, вСе сооб- щения wm.MouseMove о ее перемещении посылались исключительно главному окну программы. В Windows такое действие называется захватом мыши (mouse capture). Для захвата мыши вызывается функция SetCapturefWnd: HWnd): HWnd; которой передается дескриптор окна. После вызова функции все со- общения от мыши направляются заданному окну независимо от пози- ции курсора. Монопольная работа с мышью обязательно завершается вызовом процедуры ReleaseCapture; 0 которая освобождает мышь. В примере захват мыши производится по нажатии левой ее кнопки (в методе ответа на сообщение WMLButtonDown), а освобождение - при отпускании кнопки (в методе WMLButtonUp). Метод WMLButtonDown выполняет следующие операции: • захватывает мышь;
122 Программирование в среде Borland Pascal для Windows • получает временный контекст дисплея и сохраняет его в перемен- ной HoldDC; • перемещает указатель пера в позицию, в которой находилась мышь в момент нажатия ее левой кнопки; • добавляет в массив Points новую точку, являющуюся началом но- вой кривой. Метод WMLButtonUp делает следующее: • добавляет в массив Points “пустую” точку, завершая кривую; • освобождает временный контекст дисплея HoldDC; • освобождает мышь. Метод WMMouseMove вызывается при любом перемещении мыши в пределах рабочей области окна. После захвата мыши окно начинает получать все сообщения о ее перемещении, даже если мышь находится за пределами окна. Если мышь перемещается с нажатой кнопкой (об этом свидетельствует булевское поле ButtonDown), то метод WMMouseMove выполняет такую последовательность действий: • рисует в контексте дисплея HoldDC прямую от точки, в которой была мышь при обработке предыдущего сообщения, до точки, в ко- торой находится мышь в настоящий момент; • создает и добавляет в массив Points очередную точку кривой с координатами, соответствующими координатам мыши. Рисование прямых отрезков выполняет функция LineTo(DC: HDC; X, Y: Integer): Bool; где DC - контекст устройства; X,Y- логические координаты конеч- ной точки прямой. В качестве начальной точки всегда принимается текущая позиция в контексте устройства. После рисования линии функция LineTo изменяет координаты текущей позиции, устанавливая их равными координатам конечной точки. По этой причине последо- вательный вызов LineTo с различными значениями параметров X и Y автоматически приводит к рисованию ломаной. Добавим, что для невидимого изменения текущей позиции вывода используется функция MoveTo(DC: HDC; X, Y: Integer): Longint; где DC - контекст устройства; X,Y- новые логические координаты. Метод WMLButtonDblClk вызывается при двухкратном щелчке левой кнопки мыши. Он удаляет из коллекции Points все точки и очи- щает рабочую область окна. Метод Paint перерисовывает все линии коллекции Points, восста- навливая изображение, например после максимизации окна. Добавим, что булевская переменная ButtonDown, объявленная в объекте TMainWindow, позволяет работать согласованно и корректно функциям ответа на сообщения мыши.
Гпава 4. Интерфейс графических устройств 123 Итак, рисовать с помощью мыши вы уже умеете. Давайте теперь усложним программу MOUSEDRW так, чтобы рисовать можно было не только линии, но и геометрические фигуры, например прямоуголь- ники. В этом случае способ хранения информации о нарисованном изображении (коллекция точек), выбранный нами в MOUSEDRW, оказывается непригодным. Конечно, можно разработать структуры данных, которые позволят хранить, кроме линий, еще и прямоуголь- ные области. Но стоит задуматься, а рационален ли такой подход. Ведь далее потребуется наделить программу способностями рисовать круги, эллипсы и другие фигуры, а также сохранять и загружать изо- бражение из файла. На самом деле данная проблема уже решена в системе Windows с помощью метафайлов, которые мы рассмотрим в следующем параграфе. 4.11. МЕТАФАЙЛЫ Интерфейс GDI обеспечивает приложения не только средствами ото- бражения, но и средствами хранения графической информации. Од- ной из форм ее хранения являются метафайлы. Изображение, запи- санное в метафайл, представляет собой не растровый образ, а после- довательность команд GDI, выполнение которых позволяет воспро- извести хранимое изображение на любом графическом устройстве. Метафайл обычно ассоциирован с файлом на диске, но может быть организован и в оперативной памяти. Метафайлы особенно полезны при многократном воспроизведении изображения на устройствах графического вывода, так как обеспечивают самый мобильный спо- соб обмена графической информацией между приложениями. Физически метафайл организуется как последовательность записей переменной длины, которые соответствуют вызовам определенных GDI-функций. Каждой функции поставлен в соответствие числовой код - номер функции. Записи метафайла хранят номера функций вме- сте со значениями их параметров. Так же, как телевизионную передачу можно записать на видеокас- сету, а потом воспроизвести на экране телевизора, последователь- ность графических примитивов можно записать в метафайл, а потом воспроизвести в окне прикладной программы. Для записи изображения в метафайл программа^ должна выпол- нить следующие действия: • создать с помощью функции CreateMetaFile метафайловый кон- текст устройства для отображения операций рисования в метафайл: CreateMetaFile(Filename: PChar): THandle; где Filename - указатель на нуль-терминированную строку, содержа-
124 Программирование в среде Borland Pascal для Windows щую имя файла. Если значение этого параметра равно nil, то мета- файл создается в оперативной памяти. Функция возвращает дескрип- тор связанного с метафайлом контекста устройства (его называют метафайловым) \ • вывести в полученный контекст устройства требуемый текст и графику. С этой целью программа должна вызывать функции GDI, передавая им в качестве параметра DC дескриптор метафайлового контекста устройства. При таком рисовании Windows разрешает ис- пользовать лишь подмножество функций GDI, которые непосредст- венно предназначены для построения изображений. Список функций GDI, разрешенных для использования с метафайлами, вы можете най- ти в справочнике по Borland Pascal; • закрыть связанный с метафайлом контекст устройства вызовом функции CloseMetaFiie(DC: THandle): THandle; где DC - закрываемый контекст устройства, связанный с метафайлом. Функция возвращает дескриптор закрытого метафайла, который можно использовать для воспроизведения записанных в метафайл GDI-команд на любом графическом устройстве. Когда метафайл становится не нужен программе, он может быть удален функцией DeleteMetaFile: DeleteMetaFile(MF: THandle): Bool; где MF - удаляемый метафайл. При вызове DeleteMetaFile система Windows уничтожает все структуры данных, которые обеспечивали управление метафайлом. В результате дескриптор метафайла MF ста- новится недействительным. Если метафайл создавался в памяти, то он уничтожается. Если он был создан на диске, то при удалении мета- файла дисковый файл не уничтожается. Это позволяет использовать метафайлы, созданные одной программой, в других приложениях. В следующем фрагменте программы показано, как создать на диске метафайл с именем EXAMPLE.WMF: procedure TMyWindow.PutlmageToFile; var MetaFile: THandle; MetaFileDC, DC: TDC; begin { Создаем метафайл с именем EXAMPLE.WMF} MetaFileDC := CreateMetaFile(’EXAMPLE.WMF’); { Выводим в метафайловый контекст устройства прямоугольник} Rectangle(MetaFileDC, 10,10,100, 200); { Выводим в метафайловый контекст устройства эллипс } Ellipse(MetaFileDC, 20, 20, 90,190);
Гпава 4, Интерфейс графических устройств 125 { Выполняем другие операции рисования } { Закрываем метафайловый контекст устройства и получаем дескриптор метафайла } MetaFile := CloseMetaFile(MetaFileDC); { Удаляем метафайл } DeleteMetaFile(MetaFile); end; Чтобы воспроизвести содержимое метафайла в заданном контексте устройства, вызывается функция PlayMetaFile(DC: HDC; MF: THandle): Bool; где DC - контекст .устройства вывода; MF - метафайл, который вос- производится в контексте устройства DC. Метафайл можно воспроиз- водить любое количество раз, причем вывод может направляться на любое устройство, в том числе и в другой метафайл. Если на диске уже имеется метафайл, то с помощью функции GetMetaFile можно получить его дескриптор: GetMetaFile(Filename: PChar): THandle; где Filename - имя файла, в котором метафайл хранится на диске. Пе- редавая полученный дескриптор в функцию PlayMetaFile, можно вос- производить содержимое метафайла на устройствах вывода. Выше мы показали, как можно создать метафайл. Теперь приведем фрагмент программы, которая получает дескриптор существующего на диске метафайла с именем EXAMPLE.WMF и воспроизводит его содержимое в окне: procedure TMyWindow.GetlmageFromFile; var MetaFile: THandle; DC: TDC; begin { Получаем контекст дисплея } DC := GetDC(HWindow); {Открываем метафайл с именем EXAMPLE.WMF} MetaFile := GetMetaFile('EXAMPLE.WMF'); { Воспроизводим содержимое метафайла в окне } PlayMetaFile(DC, MetaFile); { Удаляем метафайл } ° DeleteMetaFile(MetaFile); { Освобождаем контекст дисплея } Re lease DC (HWindow, DC); end; Нетрудно заметить, что метафайл и связанный с метафайлом кон- текст устройства - это разные понятия. Метафайловый контекст уст-
126 Программирование в среде Borland Pascal для Windows ройства может быть получен исключительно функцией CreateMetaFile и используется для записи в метафайл GDI-примитивов. При его за- крытии с помощью функции CloseMetaFile программе возвращается дескриптор метафайла, который можно использовать для воспроизве- дения метафайла на устройствах графического вывода, а также в дру- гих операциях. После закрытия связанного с метафайлом контекста устройства в метафайл больше не может быть ничего записано. Расширить метафайл дополнительной информацией можно по сле- дующему алгоритму: • создать вызовом функции CreateMetaFile новый метафайловый контекст устройства; • воспроизвести в полученном контексте устройства уже сущест- вующий метафайл с помощью функции PlayMetaFile; • вывести в метафайловый контекст устройства дополнительную графическую информацию; • закрыть метафайл с помощью функции CloseMetaFile. В результате этих операций вы получите два метафайла - старый и новый. Если больше никаких действий с метафайлами вы производить не собираетесь, то их следует удалить с помощью функции DeleteMetaFile. Закончим теоретические рассуждения и перейдем к практике: пере- работаем программу MOUSEDRW так, чтобы для хранения изобра- жения использовался метафайл, расположенный в оперативной памя- ти. В результате получим программу METAFILE: program MetaFile; uses WinTypes, WinProcs {$ifdef Ver80}, Messages {$endif}, OWindows; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) ButtonDown: Boolean; HoldDC: HDC; MetaFileDC: HDC; { кнопка мыши нажата?} { вспомогательный контекст дисплея } { метафайловый контекст устройства } constructor lnit(AParent: PWindowsObject; ATitle: PChar); destructor Done; virtual; function GetClassName: PChar; virtual; procedure GetWindowClass(var AWndClass: TWndClass); virtual; procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; procedure WMLButtonDown(var Msg: TMessage); virtual wm_First + wm_LButtonDown; procedure WMLButtonUp(var Msg: TMessage); virtual wm_First + wm_LButtonllp; procedure WMMouseMove(var Msg: TMessage); virtual wm_First + wm_MouseMove; procedure WMLButtonDblClk(var Msg: TMessage); virtual wm_First + wm_LButtonDblClk; end;
Гпава 4. Интерфейс графических устройств 127 PMetaFileApp = ATMetaFileApp; TMetaFileApp = object(TApplication) procedure InitMainWindow; virtual; end; { Конструирует оконный объект и создает контекст устройства, связанный с метафайлом в оперативной памяти } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); ButtonDown := False; MetaFileDC := CreateMetaFile(nil); end; { Закрывает и удаляет метафайл при разрушении оконного объекта } destructor TMainWindow. Done; begin DeleteMetaFile(CloseMetaFile(MetaFileDC)); inherited Done; end; { Возвращает имя нового оконного класса } function TMainWindow.GetClassName: PChar; begin GetClassName := 'MetaFileWindow'; end; { Возвращает атрибуты нового оконного класса } procedure TMainWindow.GetWindowClass(var AWndClass: TWndClass); begin inherited GetWindowClass(AWndClass); with AWndClass do Style := Style and not (cs_HRedraw or cs_VRedraw) or cs_DblClks; end; { Воспроизводит в окне содержимое метафайла } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var MetaFile: THandle; begin MetaFile := CloseMetaFile(MetaFileDC); PlayMetaFile(PaintDC, MetaFile); MetaFileDC := CreateMetaFile(nil); PlayMetaFile(MetaFileDC, MetaFile); DeleteMetaFile(MetaFile); end; { Подготавливает объект к рисованию по нажатии левой^нопки мыши } procedure TMainWindow.WMLButtonDown(var Msg: TMessage); begin if not ButtonDown then begin ButtonDown := True; SetCapture(HWindow); HoldDC := GetDC(HWindow); MoveTo(HoldDC, Msg.LParamLo, Msg.LParamHi);
128 Программирование в среде Borland Pascal для Windows MoveTo(MetaFileDC, Msg.LParamLo, Msg.LParamHi); end; end; { Завершает рисование при отпускании левой кнопки мыши } procedure TMainWindow.WMLButtonUp(var Msg: TMessage); begin if ButtonDown then begin ReleaseDC(HWindow, HoldDC); Releasecapture; ButtonDown := False; end; end; { Рисует приращение линии при передвижении мыши } procedure TMainWindow.WMMouseMove(var Msg: TMessage); begin if ButtonDown then begin LineTo(HoldDC, Msg.LParamLo, Msg.LParamHi); LineTo(MetaFileDC, Msg.LParamLo, Msg.LParamHi); end; end; { Закрывает и удаляет метафайл, затем очищает рабочую область окна } procedure TMainWindow.WMLButtonDblClk(var Msg: TMessage); begin DeleteMetaFile(CloseMetaFile(MetaFileDC)); MetaFileDC := CreateMetaFile(nil); lnvalidateRect(HWindow, nil, True); end; { Создает объект главного окна программы } procedure TMetaFileApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'Using Metafiles')); end; var MetaFileApp: TMetaFileApp; begin MetaFileApp.lnit('MetaFile'); MetaFileApp.Run; MetaFileApp. Done; end. Дадим необходимые пояснения к программе. В объекте TMainWindow вместо коллекции Points объявлен дескриптор мета- файлового контекста устройства - MetaFileDC, который используется для запоминания выводимой в окно информации. Он инициализиру- ется в конструкторе окна: MetaFileDC := CreateMetaFile(nil);
Глава 4. Интерфейс графических устройств 129 Графическая информация, которая выводится в окно приложения, одновременно направляется и в контекст устройства MetaFileDC. Когда возникает необходимость перерисовать содержимое окна, вы- зывается метод Paint, который определен в программе следующим образом: procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var MetaFile: THandJe; begin MetaFile := CloseMetaFile(MetaFileDC); PlayMetaFile(PaintDC, MetaFile); MetaFileDC := CreateMetaFile(nil); PlayMetaFile(MetaFileDC, MetaFile); DeleteMetaFile(MetaFile); end; Перерисовка окна заключается в воспроизведении содержимого метафайла в контексте дисплея PaintDC. Чтобы воспроизвести мета- файл, нужно закрыть метафайловый контекст устройства и получить дескриптор метафайла. Для этого вызывается функция CloseMetaFile. Полученный дескриптор передается в функцию PlayMetaFile для вос- произведения метафайла в контексте дисплея PaintDC. Так как после вызова CloseMetaFile дескриптор метафайлового контекста устройст- ва MetaFileDC становится недействительным, его необходимо восста- новить. Это можно сделать следующим образом: создать новый мета- файловый контекст устройства, воспроизвести в нем имеющийся ме- тафайл, а затем удалить метафайл, который больше не нужен. Пере- численные действия как раз и выполняют три последних оператора метода Paint. Методы ответа на сообщения мыши предусматривают дублирова- ние выводимой в окно информации еще и в метафайле. По двухкрат- ному щелчку левой кнопки мыши происходит очистка рабочей облас- ти окна и уничтожение всей графической информации, поэтому реак- ция на сообщение WMLButtonDblClk предусматривает закрытие кон- текста устройства MetaFileDC, удаление метафайла и создание нового метафайлового контекста устройства: procedure TMainWindow.WMLButtonDblClk(var Msg: TMessage); begin 0 DeleteMetaFile(CloseMetaFile(MetaFileDC)); MetaFileDC := CreateMetaFile(nil); lnvalidateRect(HWindow, nil, True); end; На наш взгляд, программа METAFILE проще и понятнее про- граммы MOUSEDRW. Более того, она гораздо универсальнее, ведь в метафайле априори можно сохранять практически любую графику и 5 Зак. 1049
130 Программирование в среде Borland Pascal для Windows текст, а не только линии. Если потребуется расширить функциональ- ные возможности программы рисованием прямоугольников и эллип- сов, то для этого не понадобится вводить дополнительные структуры данных для их хранения и изменять работу метода Paint. 4.12. УЧЕБНЫЙ ГРАФИЧЕСКИЙ РЕДАКТОР Сейчас мы рассмотрим наиболее сложный из всех касающихся GDI примеров - программу CHART. Она демонстрирует комплексное использование средств GDI и показывает, как можно построить иерархию объектов для работы с типичными для графических редак- торов инструментами. Чтобы придать программе CHART законченность, мы ввели в нее много пока неизвестных читателю элементов (реакция на команды меню, видоизменение курсора мыши), однако это не помешает пониманию тех мест программы, в которых идет работа с GDI. (Проблемы создания меню и работа с курсором мыши рассматрива- ются в гл. 5.) Программа CHART разработана на основе программы METAFILE и представляет собой графический редактор с ограничен- ными возможностями. Она позволяет рисовать в рабочей области окна кривые, отрезки линий, заполненные и незаполненные прямо- угольники и эллипсы, а также сохранять и загружать нарисованное изображение на диск и с диска. Окно программы имеет меню, которое содержит пункты File и Tools. Команды меню File позволяют сохранить изображение в файле на диске, а потом заново его загрузить. С помощью команд меню Tools пользователь может выбрать для рисования один из следующих инструментов: • кривая (Polyline) - для рисования кривых; • прямая (Line) - для рисования отрезков прямых линий; • прямоугольник (Rectangle) - для рисования прямоугольников с прозрачной внутренней областью; • эллипс (Ellipse) - для рисования прозрачных эллипсов; • заполненный прямоугольник (Filled Rectangle) - для рисования прямоугольников с закрашенной внутренней областью; • заполненный эллипс (Filled Ellipse) - для рисования эллипсов с за- крашенной внутренней областью. Каждому инструменту соответствует определенный вид курсора мыши, который позволяет судить о том, какой инструмент в текущий момент является активным. Итак, приведем текст программы CHART:
Гпава 4. Интерфейс графических устройств 131 = 120; = cm_Tool +1; = cm_Tool + 2; = cm_Tool + 3; = cm_Tool + 4; = cm_Tool + 5; = cm_Tool + 6; { команда меню Poly } { команда меню Line } { команда меню Rectangle } { команда меню Ellipse } {команда меню Filled Rectangle} { команда меню Filled Ellipse } = 300; ? idc_Tool + 1; = idc_Tool + 2; = idc_Tool + 3; = idc_Tool + 4; = idc_Tool + 5; = idc_Tool + 6; { курсор для Poly} { курсор для Line } { курсор для Rectangle } { курсор для Ellipse } { курсор для Filled Rectangle } { курсор для Filled Ellipse } CHART.INC const idm_Menu =100; {идентификатор меню в ресурсе} idi_Chartlcon = 200; { идентификатор пиктограммы в ресурсе} { Идентификаторы команд меню } cm_Tool cm_Poly cm_Line cm_Rect cmJEIlipse cm_FillRect cm_FillEllipse { Идентификаторы курсоров в ресурсе } idc_Tool idc__Poly idc__Line idc_Rect idcJEIlipse idc_FillRect idc_FillEllipse CHART.PAS program Chart; {$R CHART.RES} {подключение файла ресурса } uses WinTypes, WinProcs {$ifdef Ver80}, Messages {$endif}, WinDos, Strings, Objects, OWindows, OStdDIgs, BWCC; {$1 CHART.INC} {включение файла констант} type PChartWindow = ATChartWindow; {Базовый объект, определяющий общие свойства граф, инструментов} PDrawTool = ATDrawTool; TDrawTool = object(TObject) Window: PChartWindow; SavedMFDC: Integer; HoldDC: HDC; OldPen: HPen; OldBrush: HBrush; LastPoint: TPoint; procedure DrawBegin(X, Y: Integer); virtual; procedure DrawTo(X, Y: Integer); virtual; procedure DrawEnd; virtual; procedure MouseDown(AWindow: PChartWindow; X, Y: Integer); procedure MouseMove(X, Y: Integer); procedure MouseUp; end; { Гсафический инструмент для рисования произвольных кривых } PPolyTool = ATPolyTool; TPolyTool = object(TDrawTool) procedure DrawBegin(X, Y: Integer); virtual; procedure DrawTo(X, Y: Integer); virtual; { окно-владелец } {индекс сохраненного контекста метафайла} {вспомогательный контекст дисплея} { первоначальное перо } { первоначальная кисть } {последняя позиция мыши при рисовании}
132 Программирование в среде Borland Pascal для Windows end; { Гсаф. инструмент для рисования фигур, задаваемых двумя точками } PBoxTool = лТВохТоо1; TBoxTool = object(TDrawTool) StartPoint: TPoint; { начальная позиция мыши при рисовании } procedure DrawBegin(X, Y: Integer); virtual; end; { Гсафический инструмент для рисования отрезков прямых линий } PLineTool = ATLineTool; TLineTool = object(TBoxTool) procedure DrawTo(X, Y: Integer); virtual; procedure DrawEnd; virtual; end; { Гсафический инструмент для рисования прямоугольников } PRectTool = ATRectTool; TRectTool = object(TBoxTool) Filled: Boolean; { прямоугольник заполненный? } constructor lnit(AFilled: Boolean); procedure DrawTo(X, Y: Integer); virtual; procedure DrawEnd; virtual; end; { Гсафический инструмент для рисования эллипсов } PEIlipseTool = ATEIIipseTool; TEIlipseTool = object(TBoxTool) Filled: Boolean; { эллипс заполненный? } constructor lnit(AFilled: Boolean); procedure DrawTo(X, Y: Integer); virtual; procedure DrawEnd; virtual; end; { Оконный объект } TChartWindow = object(TWindow) ButtonDown: Boolean; MetaFileDC: HDC; HasChanged: Boolean; TitleHead: PChar; FileName: PChar; Pen: HPen; Tools: array [1 ..6] of PDrawTool; { набор графических инструментов } CurrTool: Integer; { номер активного графического инструмента } constructor lnit(ATitle: PChar); destructor Done; virtual; function CanClose: Boolean; virtual; procedure CMFileNew(var Msg: TMessage); virtual cm_First + cm_FileNew; procedure CMFileOpen(var Msg: TMessage); virtual cm_First + cm_FileOpen; procedure CMFileSave(var Msg: TMessage); virtual cm_First + cm_FileSave; procedure CMFileSaveAs(var Msg: TMessage); virtual cm_First + cm_FileSaveAs; {кнопка мыши нажата?} { контекст отображения метафайла } { файл изменялся?} { главная составная часть заголовка окна } { имя файла } '{ перо, используемое для рисования }
Гпава 4. Интерфейс графических устройств 133 procedure CMToolsPoly(var Msg: TMessage); virtual cm_First + cm_Poly; procedure CMToolsLine(var Msg: TMessage); virtual cm_First + cmJJne; procedure CMToolsRect(var Msg: TMessage); virtual cm_First + cm_Rect; procedure CMToolsEllipse(var Msg: TMessage); virtual cm_jFirst + cm_Ellipse; procedure CMToolsFillRect(var Msg: TMessage); virtual cm_First + cm_FillRect; procedure CMToolsFillEllipse(var Msg: TMessage); virtual cm_First + cm_FillEllipse; function GetClassName: PChar; virtual; procedure GetWindowClass(var AWndClass: TWndClass); virtual; procedure LoadFile(AFileName: PChar); procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; procedure SaveFileAs(AFileName: PChar); procedure SelectTool(NewTool: Integer); procedure SetupWindow; virtual; procedure UpdateCaption; procedure WMLButtonDown(var Msg: TMessage); virtual wm_First + wm_LButtonDown; procedure WMLButtonUp(var Msg: TMessage); virtual wm_First + wm_LButtonUp; procedure WMMouseMove(var Msg: TMessage); virtual wm_First + wm_MouseMove; end; { Прикладной объект } PChartApp = ATChartApp; TChartApp = object(TApplication) procedure InitMainWindow; virtual; end; { Начинает рисование с заданной точки } procedure TDrawTool.DrawBegin(X, Y: Integer); begin end; { Продолжает рисование до заданной точки } procedure TDrawTool.DrawTo(X, Y: Integer); begin end; { Завершает рисование } procedure TDrawTool.DrawEnd; <> begin end; { Начинает рисование по нажатии кнопки мыши } procedure TDrawTool.MouseDown(AWindow: PChartWindow; X, Y: Integer); begin Window := AWindow; SetCapture(Window\HWindow); SavedMFDC := SaveDC(WindowA.MetaFileDC);
134 Программирование в среде Borland Pascal для Windows HoldDC := GetDC(WindowA. HWindow); OldPen := SelectObject(HoldDC, GetStockObject(Black_Pen)); OldBrush := SelectObject(HoldDC, GetStockObject(Null_Brush)); LastPoint.X := X; LastPoint. Y := Y; DrawBegin(X, Y); end; { Продолжает рисование при передвижении мыши с нажатой кнопкой } procedure TDrawTool.MouseMove(X, Y: Integer); begin DrawTofX, Y); LastPoint.X := X; LastPoint. Y := Y; end; { Завершает рисование при отпускании кнопки мыши } procedure TDrawTool.MouseUp; begin DrawEnd; SelectObject(HoldDC, OldPen); SelectObject(HoldDC, OldBrush); ReleaseDC(WindowA. HWindow, HoldDC); RestoreDC(WindowA.MetaFileDC, SavedMFDC); ReleaseCapture; Window := nil; end; { Начинает рисование кривой с заданной точки } procedure TPolyTool.DrawBegin(X, Y: Integer); begin SelectObject(HoldDC, WindowA.Pen); MoveTo(HoldDC, X, Y); SelectObject(WindowA.MetaFileDC, WindowA.Pen); MoveTo(WindowA.MetaFileDC, X, Y); end; { Продолжает рисование кривой до заданной точки } procedure TPolyTool.DrawTo(X, Y: Integer); begin LineTofHoldDC, X, Y); LineTo(WindowA MetaFileDC, X, Y); end; { Начинает рисование фигуры с заданной точки } procedure TBoxTool.DrawBegin(X, Y: Integer); begin StartPointX := X; StartPoint. Y := Y; SetROP2(HoldDC, R2_Not); end; { Продолжает рисование прямой до заданной точки } procedure TLineTool.DrawTo(X, Y: Integer); begin MoveTo(HoldDC, StartPointX, StartPoint.Y);
Гпава 4. Интерфейс графических устройств 135 LineTo(HoldDC, LastPoint.X, LastPoint.Y); MoveTo(HoldDC, StartPoint.X, StartPoint.Y); LineTo(HoldDC, X, Y); end; { Завершает рисование прямой } procedure TLineTooLDrawEnd; begin MoveTo(HoldDC, StartPoint.X, StartPoint.Y); LineTo(HoldDC, LastPoint.X, LastPoint.Y); SetROP2(HoldDC, R2_CopyPen); SelectObject(HoldDC, WindowA.Pen); MoveTo(HoldDC, StartPoint.X, StartPoint.Y); LineTo(HoldDC, LastPoint.X, LastPoint.Y); SelectObject(WindowA.MetaFileDC, WindowA.Pen); MoveTo(WindowA.MetaFileDC, StartPoint.X, StartPoint.Y); LineTo(WindowA.MetaFileDC, LastPoint.X, LastPoint.Y); end; { Конструирует граф, инструмент для рисования прямоугольников } constructor TRectTooLlnit(AFilled: Boolean); begin inherited Init; Filled := AFilled; end; { Продолжает рисование прямоугольника до заданной точки } procedure TRectTool.DrawTo(X, Y: Integer); begin Rectangle(HoldDC, StartPoint.X, StartPoint.Y, LastPoint.X, LastPoint.Y); Rectangle(HoldDC, StartPoint.X, StartPoint.Y, X, Y); end; { Завершает рисование прямоугольника } procedure TRectTool.DrawEnd; begin Rectangle(HoldDC, StartPoint.X, StartPoint.Y, LastPoint.X, LastPoint.Y); SetROP2(HoldDC, R2_CopyPen); SelectObject(HoldDC, WindowA.Pen); if Filled then SelectObject(HoldDC, GetStockObject(WhiteJBrush)) else SelectObject(HoldDC, GetStockObject(Null_Brush)); Rectangle(HoldDC, StartPoint.X, StartPoint.Y, LastPoint.X, LastPoint.Y); SelectObject(WindowA.MetaFileDC, WindowA.Pen); if Filled then , SelectObject(HoldDC, GetStockObject(White_Brush)) else SelectObject(WindowA.MetaFileDC, GetStockObject(Null_Brush)); Rectangle(WindowA.MetaFileDC, StartPointX, StartPoint.Y, LastPointX, LastPoint.Y); end; { Конструирует граф, инструмент для рисования эллипсов } constructor TEIlipseTool.InitfAFilled: Boolean);
136 Программирование в среде Borland Pascal для Windows begin inherited Init; Filled := AFilled; end; { Продолжает рисование эллипса до заданной точки } procedure TEIIipseTool.DrawTo(X, Y: Integer); begin Ellipse(HoldDC, StartPoint.X, StartPoint.Y, LastPointX, LastPoint.Y); Ellipse(HoldDC, StartPoint.X, StartPoint.Y, X, Y); end; { Завершает рисование эллипса } procedure TEIlipseTool.DrawEnd; begin Ellipse(HoldDC, StartPointX, StartPoint.Y, LastPointX, LastPoint. Y); SetROP2(HoldDC, R2_CopyPen); SelectObject(HoldDC, WindowA.Pen); if Filled then SelectObject(HoldDC, GetStockObject(White__Brush)) else SelectObject(HoidDC, GetStockObject(Null_Brush)); Ellipse(HoldDC, StartPointX, StartPoint.Y, LastPointX, LastPoint.Y); SelectObject(Window\MetaFileDC, WindowA.Pen); if Filled then SelectObject(WindowA.MetaFileDC, GetStockObject(White_Brush)) else SelectObject(WindowA.MetaFileDC, GetStockObject(Null_Brush)); Ellipse(WindowA.MetaFileDC, StartPointX, StartPoint.Y, LastPointX, LastPoint.Y); end; { Конструирует оконный объект и создает граф, инструменты } constructor TChartWindow.lnit(ATitle: PChar); begin inherited lnit(nil, ATitie); with Attr do begin X := 80; Y := 80; W := 500; H := 300; Menu := LoadMenu(HInstance, MakelntResource(idm_Menu)); end; ButtonDown := False; MetaFileDC := CreateMetaFile(nil); TitleHead := StrNew(ATitle); FiieName := nil; HasChanged := False; Pen := GetStockObject(Black_Pen); Tools[1] := New(PPolyTool, Init); Tools[2] := New(PLineTool, Init); Tools[3] := New(PRectTool, Init(False)); Tools[4] := New(PEIIipseTool, Init(False)); Tools[5] := New(PRectTool, Init(True)); Tools[6] := New(PEIIipseTool, Init(True));
Гпава 4. Интерфейс графических устройств 137 CurrTool := 1; end; { Уничтожает графические инструменты и разрушает оконный объект } destructor TChartWindow.Done; var I: Integer; begin for I := 1 to 6 do Dispose(Tools[l], Done); DeieteMetaFiie(CloseMetaFile(MetaFileDC)); StrDispose(FileName); StrDispose(TitleHead); inherited Done; end; { Предлагает сохранить чертеж и разрешает закрыть окно } function TChartWindow.CanClose: Boolean; var Msg: TMessage; begin CanClose := True; if HasChanged then case MessageBox(HWindow, 'Do you want to save?’, ‘Drawing has changed', mb_YesNoCancel or mbJconQuestion) of id_Yes: begin CMFileSave(Msg); CanClose := not HasChanged; end; id_Cancel: CanClose := False; end; end; { Очищает окно по команде меню File-New} procedure TChartWindow.CMFileNew(var Msg: TMessage); begin if CanClose then begin DeleteMetaFile(CloseMetaFile(MetaFileDC)); MetaFileDC := CreateMetaFile(nil); lnvalidateRect(HWindow, nil, True); HasChanged := False; StrDispose(FileName); FileName := nil; UpdateCaption; о end; end; { Открывает существующий файл по команде меню File-Open } procedure TChartWindow.CMFileOpen(var Msg: TMessage); var FName: array [O..fsPathName] of Char; begin StrCopy(FName, ’*.wmf);
138 Программирование в среде Borland Pascal для Windows if CanClose then if ApplicationA.ExecDialog(New(PFileDialog, lnit(@Self, MakelntResource(sd_FileOpen), FName))) = id_OK then LoadFile(FName); end; { Сохраняет изображение по команде меню File-Save } procedure TChartWindow.CMFileSave(var Msg: TMessage); begin if FileName = nil then CMFileSaveAs(Msg) else SaveFileAs(FileName); end; { Сохраняет изображение в файле по команде меню File-SaveAs } procedure TChartWindow.CMFileSaveAs(var Msg: TMessage); var FName: array [O..fsPathName] of Char; begin StrCopy(FName, ,f.wmf); if ApplicationA.ExecDialog(New(PFileDialog, lnit(@Self, MakelntResource(sd_FileSave), FName))) = id_OK then SaveFileAs(FName); end; { Выбирает рисование кривых по команде меню Tools-Poly} procedure TChartWindow.CMToolsPoly(var Msg: TMessage); begin SelectTool(l); end; { Выбирает рисование отрезков прямой по команде меЬю Tools-Line } procedure TChartWindow.CMToolsLine(var Msg: TMessage); begin SelectTool(2); end; { Выбирает рисование прозрачных прямоуг. по команде Tools-Rectangle } procedure TChartWindow.CMToolsRect(var Msg: TMessage); begin SelectTool(3); end; { Выбирает рисование прозрачных эллипсов по команде Tools-Ellipse } procedure TChartWindow.CMToolsEllipse(var Msg: TMessage); begin SelectTool(4); end; { Выбирает рисование заполн. прямоуг. по команде Tools-Filled Rectangle } procedure TChartWindow.CMToolsFillRect(var Msg: TMessage); begin SelectTool(5); end; { Выбирает рисование заполн. эллипсов по команде Tools-Filled Ellipse } procedure TChartWindow.CMToolsFillEllipse(var Msg: TMessage); begin SelectTool(6); end;
Гпава 4, Интерфейс графических устройств 139 { Возвращает имя нового оконного класса } function TChartWindow.GetClassName: PChar; begin GetClassName := 'Chartwindow*; end; { Возвращает атрибуты нового оконного класса } procedure TChartWindow.GetWindowClass(var AWndClass: TWndClass); begin inherited GetWindowClass(AWndClass); with AWndClass do begin Style := Style and not (cs_HRedraw or cs_VRedraw); hlcon := Load I con(H Instance, MakelntResource(idi_Chartlcon)); hCursor := LoadCursor(Hlnstance, MakelntResource(idc_Poly)); end; end; { Загружает изображение из заданного метафайла на диске } procedure TChartWindow.LoadFile(AFileName: PChar); var MetaFile: THandle; begin MetaFile := GetMetaFile(AFileName); if MetaFile = 0 then Exit; DeleteMetaFile(CloseMetaFile(MetaFileDC)); MetaFileDC := CreateMetaFile(nil); PlayMetaFile(MetaFileDC, MetaFile); DeleteMetaFile(MetaFile); lnvalidateRect(HWindow, nil, True); StrDispose(FileName); FileName := StrNew(AFileName); HasChanged := False; UpdateCaption; end; { Перерисовывает изображение в окне } procedure TChartWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var MetaFite: THandle; begin MetaFile := CloseMetaFile(MetaFileDC); PlayMetaFile(PaintDC, MetaFile); MetaFileDC := CreateMetaFiie(nil); PlayMetaFile(MetaFileDC, MetaFile); , DeleteMetaFile(MetaFile); end; { Сохраняет изображение в заданном метафайле на диске } procedure TChartWindow.SaveFileAs(AFileName: PChar); var MetaFile: THandle; begin MetaFile := CloseMetaFiie(MetaFileDC);
140 Программирование в среде Borland Pascal для Windows DeleteMetaFile(CopyMetaFile(MetaFite, AFileName)); MetaFileDC := CreateMetaFile(nil); PlayMetaFile(MetaFileDC, MetaFile); DeleteMetaFiie(MetaFiie); StrDispose(FileName); FileName := StrNew(AFileName); HasChanged := False; UpdateCaption; end; { Выбирает для рисования графический инструмент с заданным номером, помечает соответствующий пункт меню и переустанавливает курсор } procedure TChartWindow.SelectTool(NewTool: Integer); begin CheckMenultem(Attr.Menu, cm_Tool + CurrTool, mf_ByCommand or mfJJnchecked); CheckMenultem(Attr.Menu, cm_Tool + NewTool, mf_ByCommand or mf_Checked); DestroyCursor(SetClassWord(HWindow, gcw_HCursor, LoadCursor(HInstance, MakelntResource(idc_Tool + NewTool)))); CurrTool := NewTool; end; { Обновляет заголовок окна после его создания } procedure TChartWindow.SetupWindow; begin inherited SetupWindow; UpdateCaption; end; { Формирует и устанавливает заголовок окна } procedure TChartWindow.UpdateCaption; var Caption: array [0..128] of Char; begin if FileName = nil then StrECopy(StrECopy(StrECopy(Caption, TitleHead), ’ - *), ‘Untitled*) else StrECopy(StrECopy(StrECopy(Caption, TitleHead), * - ’), FileName); SetCaption(Caption); end; { Сообщает активному инструменту о нажатии кнопки мыши } procedure TChartWindow.WMLButtonDown(var Msg: TMessage); begin if not ButtonDown then begin ButtonDown := True; Tools[CurrTool]A.MouseDown(@Self, Msg.LParamLo, Msg.LParamHi); end; end; { Сообщает активному граф, инструменту об отпускании кнопки мыши } procedure TChartWindow.WMLButtonUp(var Msg: TMessage); begin
Гпава 4, Интерфейс графических устройств 141 if ButtonDown then begin Tools[CurrTool]A.MouseMove(Msg.LParamLo, Msg.LParamHi); Tools[CurrTool]A.MouseUp; ButtonDown := False; end; end; { Сообщает активному граф, инструменту о перемещении мыши } procedure TChartWindow.WMMouseMove(var Msg: TMessage); begin if ButtonDown then begin Tools[CurrTool]A.MouseMove(Msg.LParamLo, Msg.LParamHi); HasChanged := True; end; end; { Создает объект главного окна программы } procedure TChartApp.lnitMainWindow; begin MainWindow := New(PChartWindow, Init(’Chart’)); end; var ChartApp: TChartApp; begin ChartApp. Init('Chart’); ChartApp. Run; ChartApp. Done; end. Очевидно, что эта программа трудна для понимания начинающих, поэтому мы дадим пояснения к самым трудным фрагментам. Для управления графическими инструментами в программе CHART введены специальные объекты, образующие самостоятель- ную иерархию (рис. 4.4). Рис. 4.4. Иерархия графических инструментов в программе CHART
142 Программирование в среде Borland Pascal для Windows Рассмотрим назначение каждого из объектов: TDrawTool - ключевой объект в иерархии, определяющий общие для всех графических инструментов свойства; ТВохТоо! - вспомогательный объект иерархии. Он служит пред- ком всех графических объектов, задаваемых двумя точками. Терминальные объекты иерархии реализуют следующие инстру- менты: TPoiyTool - кривую; TLineTool - прямую; TRectTool - прозрачный или заполненный прямоугольник; TEHipseTool - прозрачный или заполненный эллипс. Поясним назначение полей и методов объекта TDrawTool. Поле Window хранит указатель на окно, в котором происходит за- хват мыши и рисование. Наследники TDrawTool используют поле Window для доступа к компонентам окна TChartWindow, в частности к связанному с метафайлом контексту устройства (MetaFileDC). Поле HoldDC аналогично такому же полю в программе METAFILE - оно используется для отображения графики в окне Window. Поля OldPen и OldBrush используются для сохранения существовавших в контексте HoldDC пера и кисти. Поле LastPoint содержит предыдущую позицию курсора, которая обновляется при нажатии и перемещении мыши. Все графические инструменты должны реагировать на сообщения мыши, поэтому в объекте TDrawTool определены методы реакции на мышь: MouseDown, MouseMove и MouseUp. Перечислим действия, выполняемые методом MouseDown: • инициализация поля Window адресом окна, в котором начинает- ся рисование; • захват мыши в окне Window; • сохранение метафайлового контекста устройства; • получение контекста дисплея для окна Window и сохранение его в поле HoldDC; • выбор в контексте HoldDC стандартных графических объектов - черного пера и невидимой кисти, а также сохранение в полях OldPen и OldBrush пера и кисти, которые ранее были активными в контексте HoldDC; • инициализация поля LastPoint координатами курсора, которые он имел в момент нажатия кнопки мыши; • извещение объектом самого себя о начале этапа рисования вызо- вом виртуального метода DrawBegin (под этапом рисования понима- ется период времени, в течение которого нажата кнопка мыши). Метод MouseMove активизируется по сообщению wm_MouseMove при условии, что до этого была нажата левая кнопка мыши. Он вы- полняет следующие действия: • вызывает виртуальный метод DrawTo;
Гпава 4. Интерфейс графических устройств 143 • запоминает в поле LastPoint текущие координаты мыши. Метод MouseUp выполняет операции, обратные по смыслу опера- циям метода MouseDown: • извещает свой объект о конце этапа рисования вызовом вирту- ального метода DrawEnd; • восстанавливает в контексте отображения HoldDC перо и кисть из полей OldPen и OldBrush; • освобождает контекст дисплея HoldDC; • освобождает контекст отображения метафайла, связанный с ок- ном; • освобождает мышь; • присваивает полю Window значение nil. Виртуальные методы DrawBegin, DrawTo и DrawEnd вызываются соответственно из методов MouseDown, MouseMove и MouseUp для извещения объектом самого себя о начале, продолжении и окончании рисования. В объекте TDrawTool они пустые, а в наследниках выпол- няют действия, зависящие от типа инструмента. Отметим, что перед рисованием атрибуты контекста отображения метафайла сохраняются, а после рисования восстанавливаются. Для сохранения контекста отображения используется функция SaveDC(DC: HDC): Integer; где DC - контекст устройства. Функция создает новый фрейм в стеке сохраненных контекстов устройств и копирует туда все атрибуты контекста устройства DC. При успешном выполнении SaveDC возвращает ненулевое значение, которое идентифицирует сохраненный контекст устройства. Для восстановления контекста устройства в прежнее состояние ис- пользуется функция RestoreDC(DC: HDC; SavedDC: Integer): Bool; где DC - дескриптор восстанавливаемого контекста устройства; SavedDC - идентификатор, полученный ранее с помощью функции SaveDC. При успешном завершении RestoreDC возвращает True. Кратко поясним назначение остальных объектов иерархии. Объект ТВохТоо! является наследником TDrawTool и одновремен- но предком объектов TLineTool, TRectTool и TEJlipseTool. Общей чертой наследников ТВохТоо! является то, что этап рисования для них (от вызова DrawBegin до вызова DrawEnd) соответствует лишь визу- альному подбору размеров и пропорций графического объекта, а рисование самого графического примитива выполняет виртуальный метод DrawEnd. Поле StartPoint объекта ТВохТоо! предназначено для хранения начальной точки рисования, т.е. координат курсора на мо- мент нажатия кнопки мыши. Метод DrawBegin объекта ТВохТоо!
144 Программирование в среде Borland Pascal для Windows инициализирует поле StartPoint и устанавливает необходимую рас- тровую операцию. Под растровой операцией в GDI понимается способ комбинирова- ния цвета пикселов выводимого изображения с уже существующим. Установить в контексте устройства растровую операцию позволяет функция SetROP2(DC: HDC; DrawMode: Integer): Integer; где DC - контекст устройства; DrawMode - устанавливаемая растро- вая операция. Возможные значения параметра DrawMode представле- ны в табл. 4.8. Табл. 4.8. Коды растровых операций для функции SetROP2 Код Описание R2_Black Результирующий пиксел всегда черный R2_White Результирующий пиксел всегда белый R2_Nop Цвет пиксела не изменяется: FinalColor := ScreenColor R2_Not Цвет пиксела есть результат инвертирования цвета пик- села на экране: FinalColor := not ScreenColor R2_CopyPen Цвет пиксела равен цвету пера: FinalColor := PenColor R2_NotCopyPen FinalColor := not PenColor R2_MaskPen FinalColor := PenColor and ScreenColor R2_MaskPenNot FinalColor := PenColor and not ScreenColor R2_MaskNotPen FinalColor := not PenColor and ScreenColor R2_NotMaskPen FinalColor := not (PenColor and ScreenColor) R2_MergePen FinalColor := PenColor or ScreenColor R2_MergePenN ot FinalColor := PenColor or not ScreenColor R2_MergeN otPen FinalColor := not PenColor or ScreenColor R2_N otMergePen FinalColor := not (PenColor or ScreenColor) R2_XorPen FinalColor := PenColor xor ScreenColor R2_NotXorPen FinalColor := not (PenColor xor ScreenColor) Уточним некоторые имена в табл. 4.8: FinalColor - результирую- щий цвет; PenColor - цвет пера; ScreenColor - цвет соответствующего пиксела поверхности отображения в контексте устройства. Следует учитывать, что растровые операции можно устанавливать только для растровых устройств графического вывода, таких как дис- плей; они не могут быть заданы для векторных устройств. Получить код текущей растровой операции позволяет функция GetROP2(DC: HDC): Integer;
Гпава 4. Интерфейс графических устройств 145 где DC - контекст устройства. Она возвращает одно из значений, пе- речисленных в табл. 4.8. В программе CHART применяется растровая операция R2_Not, которая инвертирует пикселы экрана при выводе графического при- митива. Если в режиме R2_Not нарисовать графический примитив повторно в одном и том же месте, то на экране восстановится перво- начальное изображение (так как по закону отрицания отрицания not not А = А). Данная закономерность используется наследниками ТВохТоо! при создании иллюзии растягивания изображения с помо- щью мыши. Анализируя работу графических объектов в программе CHART, вы обнаружите, что результатом любого перемещения мыши при на- жатой левой кнопке является вызов виртуального метода DrawTo у текущего графического объекта. В объектах TLineTool, TRectTool и TEHipseTool методы DrawTo переопределены и восстанавливают пер- воначальное изображение повторным выводом графического прими- тива, а затем рисуют графический примитив с учетом изменившейся позиции мыши. Например, метод TRectTool.DrawTo “работает” так: procedure TRectTool.DrawTo(X, Y: Integer); begin {Восстанавливаем изображение повторным выводом прямоугольника} Rectangle(HoldDC, StartPoint.X, StartPoint.Y, LastPoint.X, LastPoint.Y); {Рисуем новый прямоугольник с учетом изменившейся позиции мыши} Rectangle(HoldDC, StartPoint.X, StartPoint.Y, X, Y); end; В результате этих простых действий в объекте TRectTool достига- ется эффект растягивания прямоугольника (при этом предполагается, что установлена растровая операция R2_Not). Подобным образом работают методы TLineTool.DrawTo и TEHipseTool.DrawTo. Для фиксации графического объекта после растягивания вызыва- ется виртуальный метод DrawEnd. Алгоритм его работы в объектах TLineTool, TRectTool и TEHipseTool следующий: • восстановление изображения на экране повторным выводом гра- фического примитива; • установка текущей растровой операции R2_CopyPen; • рисование результирующего графического объекта. Рисование осуществляется одновременно в контекстах отображе- ния HoldDC и MetaFileDC. Первое обеспечивает немедленную види- мость объекта на экране, а второе - сохранение нарисованного объек- та в метафайле. Например, метод TRectTool.DrawEnd “выглядит” так: procedure TRectTool.DrawEnd; begin {Восстанавливаем изображение повторным выводом прямоугольника} Rectangle(HoldDC, StartPoint.X, StartPoint.Y, LastPoint.X, LastPoint.Y);
146 Программирование в среде Borland Pascal для Windows { Устанавливаем текущую растровую операцию R2_CopyPen } SetROP2(HoldDC, R2_CopyPen); { Подготавливаем в контексте устройства HoldDC граф, инструменты } SelectObject(HoldDC, WindowA.Pen); if Filled then SelectObject(HoldDC, GetStockObject(White_Brush)) else SelectObject(HoldDC, GetStockObject(Null_Brush)); { Рисуем прямоугольник в контексте HoldDC } Rectangle(HoldDC, StartPoint.X, StartPoint.Y, LastPoint.X, LastPoint.Y); { Подготавливаем в MetaFileDC граф, инструменты } SelectObject(WindowA.MetaFileDC, WindowA.Pen); if Filled then SelectObject(HoldDC, GetStockObject(White_Brush)) else SelectObject(WindowA.MetaFileDC, GetStockObject(Null_Brush)); { Рисуем прямоугольник в контексте MetaFileDC } Rectangle(WindowA.MetaFileDC, StartPoint.X, StartPoint.Y, LastPointX, LastPoint.Y); end; Кратко опишем поля и методы объекта TChartWindow, реализую- щего свойства главного окна программы. Некоторые из них окажутся для вас уже знакомыми, так как они практически не изменились по сравнению с предыдущими примерами. Смысл и назначение полей ButtonDown и MetaFileDC те же, что и в программе METAFILE. Булевское поле HasChanged используется для того, чтобы зафиксировать факт изменения изображения. Заголо- вок окна программы формируется из названия программы, соединен- ного через дефис с именем метафайла на диске. Поля TitleHead и FileName хранят указатели на название программы и имя метафайла (строки размещаются в динамической памяти). Поле Реп - дескриптор пера, которым происходит рисование контуров фигур. В программе CHART всегда используется тонкое черное перо, дескриптор которо- го помещается в поле Реп при конструировании окна. Поле Реп введе- но с целью облегчить дальнейшую модернизацию программы. В мас- сиве Tools хранятся адреса всех возможных инструментов рисования, доступных в окне TChartWindow. Целочисленное поле CurrTool со- держит индекс активного графического инструмента в массиве Tools. Конструктор Init создает, а деструктор Done разрушает оконный объект TChartWindow. Метод SetupWindow перекрыт с целью вызова UpdateCaption. Процедура UpdateCaption формирует и устанавливает в окне заголовок, который состоит из названия программы и соеди- ненного с ним через дефис имени редактируемого метафайла. Если редактируется новый файл, ему назначается имя Untitled. Методы ответа на сообщения мыши WMLButtonDown, WMLButtonUp и WMMouseMove фактически передают управление активному графи-
Гпава 4. Интерфейс графических устройств 147 ческому инструменту. Метод Paint окна TChartWindow ничем не от- личается от такого же метода объекта TMainWindow из программы METAFILE и выполняет полную перерисовку окна. Методы LoadFile и SaveFileAs предназначены для загрузки и сохранения метафайла. Метод CanClose управляет закрытием окна приложения и предостав- ляет пользователю возможность сохранить изображение в файл на диске, если в сеансе работы оно было изменено. С помощью SelectTool в окне выбирается активный инструмент. В объекте TChartWindow существуют методы, названия которых начинаются с символов СМ. Это методы ответа на командные сооб- щения от пунктов меню. Они вызываются, когда пользователь выби- рает в меню соответствующую команду. Создание и подключение меню, а также используемых в программе CHART курсоров и пикто- грамм, рассматривается в гл. 5. Нетрудно заметить, что имена методов СМхххх - составные. На- пример, CMFileNew - метод ответа на командное сообщение cm_FileNew, которое генерируется при выборе в меню File пункта New. Имена остальных методов ответа на команды меню строятся по аналогии. Кратко опишем эти методы: CMFileNew - очищает окно и открывает новый метафайл без име- ни (Untitled); CMFileOpen - создает диалог для выбора существующего на диске метафайла и загружает его в окно приложения; CMFileSave - сохраняет метафайл на диск. Если сохраняется мета- файл без имени (Untitled), то пользователю предлагается назначить ему имя; CMFileSaveAs - сохраняет метафайл с новым именем; CMToolsPoly - делает активным объектом кривую; CMToolsLine - делает активным объектом прямую; CMToolsRect - делает активным объектом прямоугольник; CMToolsEllipse - делает активным объектом эллипс; CMToolsFillRect - делает активным объектом заполненный пря- моугольник; CMToolsFillEllipse - делает активным объектом заполненный эл- липс. $ 4.13. СКРОЛЛИНГ ИЗОБРАЖЕНИЙ В ОКНЕ Важнейшим элементом работы любой Windows-программы является просмотр изображений в окне. В большинстве случаев вам не удастся вывести в окно все изображение (даже если окно раскрыто на весь экран). Когда размеры изображения значительно превосходят разме-
148 Программирование в среде Borland Pascal для Windows ры окна, лучшее, что можно сделать, - это организовать прокрутку (скроллинг) изображения. При скроллинге в окне видна только часть всей картины. Передвигая изображение в окне, пользователь может последовательно получить доступ к любой его части. Чаще всего скроллинг применяется для просмотра текстовых файлов и больших растровых изображений. Для управления скроллингом в Windows используются полосы (линейки) скроллинга (scroll bars). Выбирая курсором мыши их элемен- ты, пользователь прокручивает изображение. Например, щелчки мы- шью на кнопках со стрелками прокручивают “информативные стро- ки” окна, а щелчки мышью на самой линейке прокрутки (но не на ползунке) - “информативные страницы” (понятия строки и страницы существуют для скроллинга и по вертикали, и по горизонтали). Пере- мещая ползунок, можно прокрутить изображение на любое число информативных строк или страниц. Кроме того, изображение можно прокрутить, если в рабочей области окна нажать кнопку мыши и, удерживая ее, переместить мышь за границу окна. Такое поведение мы будем называть автопрокруткой. Для создания в окне полосы скроллинга нужно установить для ок- на стили ws_HScroll и (или) ws_VScroll. Флаг ws_HScroll управляет созданием горизонтальной полосы прокрутки, а флаг ws_VScroll - созданием вертикальной. Создаваемые таким образом полосы про- крутки называются стандартными. Стандартные полосы скроллинга относятся к системной области окна, следовательно, их видимость и реакция на мышь обеспечиваются самой системой Windows. В программах на OWL окна крайне просто программируются для прокрутки любых изображений. Наследники TWindow выполняют эту задачу с помощью объекта TScroller. Объект TScroller автоматизирует просмотр содержимого окна. В его элементах данных хранятся следующие параметры скроллинга: • XUnit, YUnit - единицы скроллинга по горизонтали и вертикали. Единица скроллинга задает дискретность изображения и выбирается в зависимости от типа выводимой информации/Например, при выводе текста в качестве значений XUnit и YUnit удобно взять средние ши- рину и высоту символа. Установить значения XUnit и YUnit можно с помощью метода SetUnits. В единицах скроллинга задаются другие параметры - текущая позиция просмотра, размеры строки, страницы и диапазоны скроллинга; • XPos, YPos - текущая позиция просмотра в единицах скроллинга; • XLine, YLine - размеры “информативной строки” в единицах скроллинга. По умолчанию их значения равны 1; • XPage, YPage - размеры “информативной страницы” в единицах скроллинга. По умолчанию значения XPage и YPage вычисляются исходя из размеров рабочей области окна;
Гпава 4. Интерфейс графических устройств 149 • XRange, YRange - общее число прокручиваемых единиц по гори- зонтали и вертикали (диапазоны скроллинга). Диапазоны скроллинга могут быть в любой момент изменены с помощью метода SetRange. Организовать скроллинг в окне очень просто. Для этого в конст- рукторе окна создается объект типа TScroller и его адрес заносится в поле Scroller (определено в TWindow). Кроме того, в поле Attr.Style устанавливаются стили ws_HScroll и ws_VScroll, чтобы окно имело полосы скроллинга. Например: constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); Scroller := New(PScroller, lnit(@Self, 8, 15, 80, 60)); Attr.Style := Attr.Style or ws_VScroll or ws_HScroll; end; Конструктору Init объекта TScroller передаются указатель на скро- лируемое окно и начальные значения для параметров XUnit, YUnit, XRange и YRange. Размеры строк и страниц принимают значения по умолчанию. В методе Paint оконный объект может выводить графическую ин- формацию, даже не зная о том, что в окне выполняется скроллинг. Конечно, если изображение слишком велико, то рисовать его полно- стью ради небольшой видимой части нецелесообразно. При необхо- димости перерисовку окна можно соптимизировать, осуществляя вы- вод только той части изображения, которая видна в окне. Для этого применяется метод IsVisibleRect объекта TScroller, который проверяет, видна ли в окне хотя бы часть заданного прямоугольника. Программа SCROLL демонстрирует построение полноценного приложения, которое выводит в окно скроллируемый текст. Скрол- линг может выполняться в горизонтальном и вертикальном направ- лениях с помощью полос скроллинга или автопрокруткой. Кроме того, в программе реализован скроллинг с помощью клавиатуры: program Scroll; uses WinTypes, WinProcs {$rfdef Ver80}, Messages {$endif}, WinDos, Strings, Objects, OWindows; type PTextLines = ATTextLines; TTextLines = object(TCollection) о procedure Freeltem(ltem: Pointer); virtual; end; PMainWindow = ATMainWindow; TMainWindow = obJect(TWindow) TextLines: PTextLines; { коллекция текстовых строк} constructor lnit(AParent: PWindowsObject; ATitle: PChar); destructor Done; virtual; function LoadFile(FileName: String): Integer;
150 Программирование в среде Borland Pascal для Windows procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; procedure WMKeyDown(var Msg: Tmessage); virtual wm_First + wm_KeyDown; end; TScrollApp = object(TApplication) procedure InitMainWindow; virtual; end; { Освобождает элемент коллекции строк} procedure TTextLines.Freeltem(ltem: Pointer); begin StrDispose(PChar(ltem)); end; { Конструирует оконный объект и загружает строки из файла } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); var MaxLineLen: Integer; begin inherited lnit(AParent, ATitle); TextLines := New(PTextLines, lnit(100,10)); MaxLineLen := LoadFile(’SCROLL.PAS’); if Status <> 0 then Exit; Scroller := New(PScroller, lnit(@Self, 8,15, MaxLineLen, TextLines\Count)); Attr.Style := Attr.Style or ws_VScroll or ws_HScroll; end; { Уничтожает коллекцию строк и разрушает оконный объект } destructor TMainWindow. Done; begin if TextLines <> nil then Dispose(TextLines, Done); inherited Done; end; { Возвращает максимальное из двух чисел } function Мах(А, В: Integer): Integer; begin if А > В then Max := A else Max := B; end; { Загружает строки из заданного файла и помещает их в коллекцию } function TMainWindow.LoadFile(FileName: String): Integer; var TextLine: array [0..255] of Char; TextFile: Text; MaxLen: Integer; begin LoadFile := 1; Assign(TextFile, FileName); {$1-} Reset(TextFile); if lOResult о 0 then b£gin MessageBox(0, ‘File SCROLL.PAS not found.’, nil, mbJconHand or mb_Ok); Status := emJnvalidMainWindow; Exit; end;
Гпава 4. Интерфейс графических устройств 151 MaxLen :=1; while not Eof(TextFile) do begin ReadLn(TextFile, TextLine); MaxLen := Max(MaxLen, StrLen(TextLine)); TextLinesA.lnsert(StrNew(TextLine)); end; Close(TextFile); LoadFile := MaxLen; end; { Устанавливает единицы скроллинга и отображает строки текста } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var TheLine: Integer; TextMetrics: TTextMetric; { Выводит одну строку текста } procedure DrawLine(TheText: PChar); far; begin if TheText <> nil then TextOut(PaintDC, 0, TheLine * ScrollerA.YUnit, TheText, StrLen(TheText)); Inc(TheLine); end; begin GetTextMetrics(PaintDC, TextMetrics); ScrollerA.SetUnits(TextMetrics.tmAveCharWidth, TextMetrics.tmHeight); TheLine := 0; TextLinesA.ForEach(@DrawLine); end; {Выполняет скроллинг при нажатии клавиш на клавиатуре} procedure TMainWindow.WMKeyDown(var Msg: TMessage); begin with Scrolled do case Msg.WParam of vk_Up: ScrollBy(0, -YLine); vk_Down: ScrollBy(0, YLine); vk_Left: ScrollBy(-XLine, 0); vk_Right: ScrollBy(XLine, 0); vk_Home: ScrollTo(0, 0); vk_End: ScrollTo(0, YRange - YPage); vk_Prior: ScrollBy(0, -YPage); vk_Next: ScrollBy(0, YPage); о end; Msg.Result := 0 {клавиша обработана} end; { Создает объект главного окна программы } procedure TScrollApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Scrolling Demo*)); end;
152 Программирование в среде Borland Pascal для Windows var ScrollApp: TScrollApp; begin ScrollApp.lnit('ScroH'); ScrollApp.Run; ScrollApp. Done; end. Объект TTextLines предназначен для хранения загруженных из файла строк текста. Конструктор оконного объекта TMainWindow создает экземпляр TTextLines, заполняет его строками из файла с име- нем SCROLL.PAS (вызывая метод LoadFile) и создает объект скрол- линга TScroller. Единица скроллинга устанавливается равной 8 пиксе- лам по горизонтали и 15 по вертикали. В данный момент эти значения могут быть любыми, так как в методе Paint они переустанавливаются. Диапазоны скроллинга задаются равными максимальной длине стро- ки по горизонтали (значение, возвращаемое методом LoadFile) и об- щему числу строк текстового файла по вертикали. Деструктор TMainWindow уничтожает только объект TTextLines, так как разрушение объекта TScroller осуществляет деструктор предка (объект TWindow). Метод Paint устанавливает единицы скроллинга и выводит строки текстового файла в контекст дисплея. Наименьшая единица текста - буква, поэтому в качестве горизонтальной и вертикальной единиц скроллинга выбираются соответственно средние ширина и высота букв шрифта. Для получения параметров шрифта в методе Paint вы- зывается Windows-функция GetTextMetrics. Она возвращает запись типа TTextMetric, содержащую необходимую информацию о текущем шрифте контекста устройства. Интересующие нас характеристики находятся в полях tmAveCharWidth и tmHeight. Они передаются функции SetUnits объекта Scroller. После этого строки текста выво- дятся в контекст дисплея с помощью вложенной far-процедуры DrawLine и метода-итератора ForEach. Метод WMKeyDown в объекте TMainWindow обеспечивает при- вязку клавиш перемещения курсора, смены страниц, а также клавиш Ноте и End к скроллингу текста. Для непосредственного изменения позиции просмотра в объекте TScroller существуют методы ScrollTo и ScrollBy. Каждый из них имеет два параметра типа Longint, задавае- мых в единицах скроллинга. В метод ScrollTo передаются абсолютные значения X и Y новой позиции просмотра, а в метод ScrollBy - смеще- ния Dx и Dy относительно текущей позиции. На этом мы заканчиваем рассмотрение GDI и переходим к не ме- нее важной, но, в принципе, более простой теме - ресурсам Windows.
5 РЕСУРСЫ WINDOWS 5.1. ПОНЯТИЕ РЕСУРСОВ Нравится это читателю или нет, ро интерфейс современных программ в очень большой степени формализован и стандартизирован. И стан- дарт этот - Windows. Поэтому программист должен хорошо знать элементы интерфейса (меню, окна всех сортов, кнопки, акселераторы и т.д.) этой системы, уметь их создавать и управлять ими. В Windows элементы интерфейса представлены в виде так называемых ресурсов, без обращения к которым не может работать даже самая простая про- грамма. Ресурсы Windows - это специальный тип данных, предназначенный для представления и использования в прикладных программах стан- дартных элементов пользовательского интерфейса. В Windows дос- тупны следующие виды ресурсов: • меню (menu); • акселераторы (accelerator); • пиктограммы (icon); • курсоры (cursor); • растровые изображения (bitmap); • таблицы строк (string table); • панели диалогов (dialog box); • шрифты (font); • информация о версии программы (version information). Кроме стандартных, программист может создавать и использовать при формировании интерфейса свои собственные типы ресурсов. Хотя ресурсы прикладной программы являются составной частью ЕХЕ-файла, они располагаются и используются отдельно от сегмен- тов данных и кода, что позволяет экономить оперативную память (ОП). Ресурсы загружаются в ОП только при необходимости, а все остальное время находятся на диске. При нехватке ОП Windows уда- ляет ненужные в данный момент ресурсы. Ресурсы обладают таким важным качеством, как разделяемость. В Windows можно запустить несколько экземпляров одной программы, и все они будут использовать только одну копию ресурсов в памяти.
154 Программирование в среде Borland Pascal для Windows Благодаря тому, что ЕХЕ-файл имеет специальную структуру и универсальный формат хранения ресурсов, программист может ре- дактировать существующие ресурсы, копировать их из одной про- граммы в другую, просматривать. Для выполнения этих действий существуют специальные программы - редакторы ресурсов. Наиболее популярный из них - Resource Workshop, который входит в поставку Borland Pascal 7.0. С его помощью, например, можно легко перенести понравившуюся картинку, пиктограмму или панель диалога из одной программы в другую. 5.2. СОЗДАНИЕ И ПОДКЛЮЧЕНИЕ РЕСУРСОВ Очевидно, что создание и подключение ресурсов является важнейшим этапом разработки любой Windows-программы. Это можно сделать одним из следующих способов: • скопировать уже существующий ресурс из другого приложения; • создать ресурсы с помощью редактора ресурсов; • создать и скомпилировать файл описания ресурсов (resource script file); о создавать ресурсы динамически в процессе исполнения програм- мы (как это делает Resource Workshop во время своей работы). Результатом создания ресурсов является двоичный файл с расши- рением RES, который содержит код доступных программе ресурсов и подключается к Pascal-программе на этапе ее сборки (link). В самом общем случае при разработке ресурсов создается файл описания ресурсов. Это текстовый файл с расширением RC, содержа- щий ресурсы Windows, описанные с помощью специальных операто- ров. В RC-файле определяются меню, акселераторы клавиш, таблицы строк, панели диалогов. Однако некоторые виды ресурсов (пиктограммы, курсоры, растровые изображения) требуют хранения графических и других специально кодированных образов, которые трудно представить в виде осмысленного текста на языке описания ресурсов. В этом случае система Windows определяет специальные форматы файлов для хранения каждого вида таких ресурсов. Пикто- граммы хранятся в файлах с расширением ICO, курсоры - в файлах, имеющих расширение CUR, растровые изображения - в ВМР-файлах, а шрифты - в FNT-файлах. На наш взгляд, наиболее удобным средст- вом создания такого рода ресурсов является Resource Workshop. Когда все необходимые файлы ресурсов, включая RC-файл, созданы, их необходимо скомпилировать. После компиляции файла описания ресурсов вместе со специализированными файлами пикто- грамм, курсоров, растровых изображений и шрифтов получается файл ресурса с расширением RES. Скомпилировать ресурсы можно компи-
Глава 5. Ресурсы Windows 155 лятором ресурсов RC.EXE фирмы Microsoft или его аналогом - компилятором BRC.EXE фирмы Borland (входит в состав Borland Pascal 7.0). Чтобы избежать хлопот, связанных с компиляцией файла описания ресурсов, следует воспользоваться программой Resource Workshop, которая редактирует все виды ресурсов Windows. Работать с ней дос- таточно просто и удобно, причем создание ресурсов становится при- ятным и увлекательным занятием. Подчеркнем, можно использовать Resource Workshop как для непосредственного создания RES-файла ресурсов, так и для создания RC-, CUR-, ICO-, BMP- и FNT- файлов. Если вы используете в своей работе Resource Workshop, создание RC- файлов имеет смысл лишь в случае документирования программы. При создании каждому ресурсу назначается числовая или строко- вая константа - идентификатор ресурса. Он необходим для доступа к ресурсу из прикладной программы. Идентификаторы ресурсов приня- то объявлять в разделе const как именованные константы. Поэтому, наряду с RES-файлом, обычно создается файл объявления констант, имеющий расширение INC. Он содержит объявления на языке Pascal идентификаторов всех доступных в программе ресурсов и включается в текст программы с помощью директивы $1. Возможно также созда- ние полноценного PAS-модуля языка Borland Pascal, содержащего только объявления констант. Создание INC- и PAS-файлов объявле- ния констант поддерживается и редактором Resource Workshop, и компилятором BRC.EXE (но не поддерживается компилятором RC.EXE!). Когда RES-файл ресурсов создан, его необходимо подключить к разрабатываемой программе. Так как Borland Pascal не поддерживает работу с проектами, то, чтобы подключить к программе объектный модуль (OBJ-файл) или библиотеку (LIB-файл), написанную на Ассемблере или языке С, надо вставить в текст программы директиву компилятора $L с указанием имени подключаемого модуля. Файл ресурсов подключается так же, но используется директива $R. В па- раметре директивы $R указывается имя файла, содержащего подклю- чаемый ресурс. В программе подключение ресурсов может выглядеть следующим образом: program MyProgram; {$R RESFILE1.RES} {первый файл ресурса } о {$R RESFILE2.RES} { второй файл ресурса } {$! CONSTDEF. INC} { файл определения констант } Если файл ресурса подключается внутри модуля, то компилятор просто запоминает его имя в создаваемом объектном TPW-модуле, причем в этот момент файл ресурса может и не существовать. На эта- пе редактирования связей (linking) все файлы ресурсов из всех модулей
156 Программирование в среде Borland Pascal для Windows и основной программы собираются вместе и подключаются к резуль- тирующему ЕХЕ-файлу, образуя ресурсы Windows-программы. Ком- поновщик записывает ресурсы в ЕХЕ-файл в двоичном виде. 5.3. ПОДГОТОВКА НОВОГО ФАЙЛА РЕСУРСОВ Начинающие пользователи Resource Workshop часто сталкиваются с трудностями при разработке своего первого файла ресурсов. Поэтому мы приводим описание минимальной последовательности шагов, которые нужно сделать, чтобы создать в Resource Workshop новый проект и подготовить его к работе. В соответствующих параграфах мы рассмотрим, как добавить в проект такие ресурсы, как меню, таб- лицы акселераторных клавиш, пиктограммы, курсоры, растровые изображения, таблицы строк, панели диалогов. Предлагаемая ниже последовательность действий не является строгой и предназначена для начинающих. Итак, запускаем Resource Workshop и выбираем команду File- New project... . В появившемся окне (рис. 5.1) указываем для нашего файла проекта расширение .RC. После этого меню дополняется новы- ми пунктами и в правом верхнем углу главного окна появляется небольшое окно с заголовком untitled.rc. Это окно называется окном проекта, в нем отображается список содержащихся в проекте ресур- сов. Сначала окно проекта является пустым, так как ни одного ресур- са мы еще не создали. В дальнейшем, чтобы изменить какой-либо существующий ресурс, просто выберите его в этом окне мышью. Наш проект пока безымянный, поэтому окно проекта имеет заголовок untitled.rc. Прежде чем продолжить работу, желательно Рис. 5.1. Окно New project редактора ресурсов
Глава 5. Ресурсы Windows 157 присвоить проекту имя, т.е. связать его с файлом на диске. Для этого выбираем команду меню File-Save file as... и вводим в диалоговом окне с заголовком Save file as имя нового файла, например MYRES (расширение можно не указывать). При этом тип файла в строке File type должен иметь значение RC resource script. В результате создается пустой файл описания ресурсов с именем MYRES.RC. Теперь активизируем команду меню File-Preferences... .На экране появляется диалоговый блок Preferences (рис. 5.2). В группе Multi-save устанавливаем пометку на поле .RES. Имя файла справа от него должно быть MYRES.RES. Тем самым мы ука- зываем редактору Resource Workshop, что исходный файл описания ресурсов MYRES.RC и двоичный файл ресурсов MYRES.RES долж- ны обновляться одновременно. Заметьте, что строка ввода Include path является тусклой, а значит, недоступной. Она служит для ввода вклю- чаемых в поиск каталогов и может быть изменена только тогда, когда в Resource Workshop еще не открыт проект. Рис. 5.2. Окно Preferences редактора ресурсов
158 Программирование в среде Borland Pascal для Windows Закрываем окно Preferences кнопкой Ок и выбираем команду меню File-Add to project.... Она позволяет добавить к проекту другие файлы самых различных типов, в частности указать файл, в котором мы бу- дем сохранять символьные (“говорящие”) идентификаторы создавае- мых ресурсов. Они объявляются как константы в разделе const Pascal- программы. Чтобы идентификаторы сохранились в INC-файле на языке Pascal, достаточно выбрать в списке с названием File type строку INC pascal constant include, а в строке File name указать имя MYRES.INC. Этот файл будет использоваться в дальнейшем вместе с нашим проектом и должен подключаться в программе на Borland Pascal с помощью директивы $1. Часто в проект включают еще файл OWINDOWS.INC, в котором определены идентификаторы, широко используемые при программировании на OWL. В программе файл OWINDOWS.INC подключать не надо: объявления, сделанные в этом файле, становятся видимы программе при упоминании OWINDOWS в списке подключаемых с помощью uses модулей. Альтернативным способом хранения идентификаторов является модуль языка BPW, содержащий только объявления констант. Его можно указать, если в списке File type выбрать строку PAS pascal constant unit. Такой модуль необходимо скомпилировать и подклю- чить в разделе uses Pascal-программы. Этот способ хорош для боль- ших программ, когда одни и те же идентификаторы используются в нескольких модулях. В наших простых примерах идентификаторы будут сохраняться в INC-файлах. Командой File-Add to project... можно расширить проект другими, уже существующими, ресурсами. Чтобы добавить файл MYRES.INC к проекту, достаточно нажать в окне Add file to project кнопку Ok и на вопрос, следует ли его создать, ответить Yes (Да). Управлять списком идентификаторов удобно из окна Identifiers, которое создается по команде меню Resource-Identifiers.... Сохраним подготовленный к работе проект командой меню File- Save project. После этого можно смело выходить из Resource Workshop. На диске останутся результаты работы: файлы MYRES.RC, MYRES.INC, MYRES.RES и MYRES.RWS. В файле с расширением RWS сохранятся текущие настройки Resource Workshop. Если его удалить, то проект поврежден не будет, но сделанные в редакторе изменения различных параметров потеря- ются и для всех опций будут установлены значения, принятые по умолчанию. По этой простой причине рекомендуется удалять RWS- файл только тогда, когда программа, использующая ресурсы, уже написана и отлажена. Для того чтобы впоследствии продолжить работу с проектом MYRES.RC, его нужно открыть командой File-Open project....
Глава 5. Ресурсы Windows 159 5.4. МЕНЮ 5.4.1. ОСНОВНЫЕ ПОНЯТИЯ Всплывающие меню являются одним из наиболее характерных атрибу- тов Windows-интерфейса. Механизм управления меню встроен в сис- тему, поэтому у программиста крайне редко возникают проблемы с разработкой и подключением меню к программе. Меню состоит из пунктов (элементов), которые подразделяются на терминальные и нетерминальные, С каждым терминальным пунктом меню (menu item) связана команда, которая посылается прикладной программе в форме сообщения при выборе данного пункта меню. Выбор нетерминального пункта меню приводит к появлению всплы- вающего меню более низкого уровня (pop-up menu), которое также может.содержать терминальные и нетерминальные элементы. Меню могут иметь любой уровень вложенности. Windows поддерживает горизонтальные, вертикальные и колончатые меню (горизонтальные и вертикальные меню предстарляют собой частный случай колонча- тых). Пункты меню могут быть помеченными (checked) и непомеченными (unchecked). Если пункт меню помечен, то слева от него имеется хо- рошо заметная “птичка”. Кроме того, пункт меню может находиться в одном из трех состояний: разрешен (enabled), запрещен (disabled), тусклый (grayed). В случае, когда пункт меню разрешен, его мо5кно выбрать, причем если с ним связана команда, то она будет послана программе, а если другое всплывающее меню, - то оно будет открыто. Если пункт меню запрещен, то его выбор не приводит ни к какому результату. Визуаль- но запрещенный пункт меню выглядит так же, как и разрешенный. Состояние “тусклый” равносильно состоянию “запрещен”, однако в отличие от запрещенного тусклый пункт меню визуально отличается от остальных тем, что он виден недостаточно отчетливо. Меню, как и всякий ресурс, идентифицируется именем (строкой или целым числом). Перед использованием меню должно быть загру- жено функцией LoadMenu, которая возвращает дескриптор объекта меню в оперативной памяти (не путать с идентификатором меню!). После загрузки полученный дескриптор должен использоваться во всех операциях с данным меню. о 5.4.2. РАЗРАБОТКА МЕНЮ НА БАЗЕ RESOURCE WORKSHOP В параграфе 5.3 были описаны подготовительные действия, которые требуется выполнить при разработке нового файла ресурсов. Верни- тесь к этой теме и подготовьте по приведенному там алгоритму файл описания ресурсов MENU.RC вместе с двоичным файлом ресурсов
160 Программирование в среде Borland Pascal для Windows MENU.RES и включаемым файлом MENU.INC языка Pascal. Удосто- верьтесь, что к проекту присоединен файл OWINDOWS.INC. Теперь самое время добавить в этот проект ресурс меню. Алгоритм этого процесса включает несколько шагов. Сначала активизируем команду меню Resource-New... и зададим тип ресурса MENU в списке Resource type, в результате откроется ре- дактор меню. Слева в окне редактора располагается панель диалога, предназначенная для редактирования отдельных пунктов меню. В поле Item text вводится название пункта меню, а в поле Item id указы- вается код соответствующей ему команды. Группа кнопок Item type управляет типом элемента меню: всплывающее подменю (Pop-up), команда (Menu item) или разделитель (Separator). Кнопки в группе Break before позволяют разбивать пункты меню по колонкам. Группа кнопок Initial state задает начальное состояние пункта меню: разре- шен, запрещен или тусклый, а также помечен или не помечен. Справа внизу отображается текстовое описание ресурса (оно по- мещается в файл MENU.RC). Справа вверху расположено тестовое меню, которое наглядно показывает то, что мы создаем. Оно позволя- ет опробовать меню в любой момент редактирования. На рис. 5.3 показано, как будет выглядеть в редакторе результирующее меню, которое и является целью разработки. В поле Item text записываем строку &File и нажимаем клавишу Enter. После этого тестовое меню корректируется в соответствии с новым именем: в нем появляется новый пункт File. Символ “амперсант” (&) делает следующую за ним букву подчеркнутой. Под- черкнутая буква может использоваться в комбинации с Alt для быст- рого выбора пункта меню с клавиатуры. Например, пункт File можно быстро выбрать комбинацией Alt+F. Теперь с помощью клавиш Ctrl+ф выделим в окне текстового опи- сания ресурса строку MENUITEM ’’Item”. Вместо Item набираем &New и нажимаем Tab для перехода в поле Item id, которое служит для ввода команды, генерируемой данным пунктом меню. Первона- чально оно содержит значение, которое, скорее всего, равно 101. Ис- пользование для команды непосредственного числового значения - это плохой стиль, поэтому назначим нашему пункту New в качестве команды именованную константу. Изменяем 101 на cmJFileNew и нажимаем клавишу Enter. Константа cm_FileNew объявлена в модуле OWINDOWS.INC, поэтому редактор распознает ее, как уже сущест- вующую. Добавим в меню File новый пункт. Для этого выделим, нажимая клавиши Ctrl-ьТ и Ctrl+Ф, строку MENUITEM ”&New” и нажмем кла- вишу Insert (или выберем в меню Resource Workshop команду Menu- New menu item). Новый пункт меню помещается после выделенного. В поле Item text введем строку &Ореп, а в поле Item id - cmJFil eOpen.
Глава 5. Ресурсы Windows 161 Рис. 5.3. Редактор меню в Resource Workshop Аналогичным образом добавим пункты &Save (команда cm_FileSave) и Save &as (команда cm_FileSaveAs). Поместим после пункта Save &as разделитель. С помощью клавиш Ctrl+T и Ctrl+Ф делаем текущим пункт Save &as. Нажимая клавишу Insert, добавляем в меню новый пункт и выбираем в группе Item type кнопку Separator. Данный пункт меню становится разделителем, в качестве которого выступает горизонтальная линия. После разделителя добавляем пункт E&xit, генерирующий команду cm_Exit (но не cm_FileExit!). Так же, как и все предыдущие назначен- ные нами идентификаторы, идентификатор cmJExit объявлен в OWINDOWS.INC. На следующем шаге добавляем второе всплывающее меню с име- нем &Не1р. Для этого сначала выбираем строку__EndPopup___в окне текстового описания ресурса, а затем нажимаем Ctrl+P (или выбираем команду Menu-New popup в меню Resource Workshop). Если вы допус- тили ошибку, воспользуйтесь командой Edit-Undo, чтобы ее испра- вить, или удалите с помощью клавиши Delete ошибочную строку в текстовом описании ресурса. 6 Зак. 1049
162 Программирование в среде Borland Pascal для Windows Теперь поместим в меню Help пункт About с командой cm_About. Команда cm_About еще не объявлена ни в одном из включаемых INC- файлов, поэтому Resource Workshop спрашивает, следует ли создать новый идентификатор с таким именем и определить для него значе- ние. Отвечаем утвердительно. На экране появляется окно, в котором предлагается ввести значе- ние константы cm_About. Обычно команда cm_About имеет код 999. Помещаем ее в файл MENU.INC. Когда константа определена, про- исходит возврат в редактор меню. Чтобы посмотреть, как работает созданное нами меню, переходим в тестовое меню, находящееся справа вверху. При передвижении по нему все остальные поля редактора отображают информацию о теку- щем в данный момент пункте. Меню создано! Теперь можно закрыть редактор меню, нажав кла- виши Ctrl+F4. Как видно в окне menu.rc, проект пополнился новым ресурсом ме- ню с идентификатором MENU_1. При именовании ресурсов удобнее использовать числовые константы. Чтобы переименовать ресурс, вы- деляем в окне menu.rc строку MENU_1 и выбираем команду Resource- Rename... . Вводим идентификатор idm_Menu и нажимаем клавишу Enter. Редактор Resource Workshop выдаст предупреждение о том, что константа idm_Menu является новой, и предложит ее определить. От- вечаем утвердительно и в появившемся диалоговом окне задаем 100 в качестве значения поля Value. Значение 100 выбрано произвольно. Новую константу помещаем в файл MENU.INC. Сохраняем проект с помощью команды File-Save project и выходим из Resource Workshop. Теперь на диске имеются все необходимые файлы: MENU.INC, MENU.RC и MENU.RES. Файл MENU.INC содержит описания констант и выглядит так: const cm_About =999; idm_Menu = 100; Он должен быть включен в текст программы с помощью директивы {$1 MENU.INC} после секции uses. Файл MENU.RC содержит текстовое описание ресурса меню. В качестве одного из включаемых файлов он содержит MENU.INC. Содержание файла MENU.RC понятно без комментариев: include "owindows.inc" include "menu.inc" idm_Menu MENU BEGIN POPUP "&File" BEGIN
Глава 5. Ресурсы Windows 163 MENUITEM "&New", cm_FileNew MENUITEM "&Open", cm_FileOpen MENUITEM "&Save", cm_FileSave MENUITEM "Save &as", cm_FileSaveAs MENUITEM SEPARATOR MENUITEM "E&xit", cm_Exit END POPUP "&Help" BEGIN MENHITEM "«About...", cm_About END END Файл MENU.RES содержит двоичное представление ресурса меню. Он должен подключаться в программе с помощью директивы {$R MENU.RES}, которая обычно помещается сразу после заголовка program. 5.4.3. РЕАКЦИЯ НА КОМАНДЫ МЕНЮ Необходимые для меню ресурсы созданы. Как практически использо- вать их в реальной программе? Покажем это на примере достаточно простой программы MENU: program Menu; {$R MENU.RES} uses WinTypes, WinProcs, OWindows; {$1 MENU. INC} type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) constructor lnit(AParent: PWindowsObject; ATitle: PChar); procedure CMFileNew(var Msg: TMessage); virtual cm_First + cm_FileNew; end; PMenuApp = ATMenuApp; TMenuApp = object(TApplication) procedure InitMainWindow; virtual; end; { Конструирует оконный объект и загружает из ресурса меню } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin t inherited lnit(AParent, ATitle); Attr.Menu ;= LoadMenu(Hlnstance, PChar(idm_Menu)); end; { Реагирует на команду меню File-New выдачей на экран сообщения } procedure TMainWindow.CMFileNew(var Msg: TMessage); begin MessageBox(HWindow, '"File-New" Command Selected', 'Message', mb_Ok); end;
164 Программирование в среде Borland Pascal для Windows { Создает объект главного окна программы } procedure TMenuApp. InitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Menu Demo')); end; var MenuApp: TMenuApp; begin MenuApp.lnit(’Menu'); MenuApp. Run; MenuApp.Done; end. Откомпилируйте и запустите программу на выполнение. На экране появится окно с заголовком Menu Demo. Окно имеет меню. Выбор в нем любой команды, кроме File-New и File-Exit, безрезультатен, так как они “не наполнены содержанием”. По команде File-New на экран выдается сообщение "File-New” Command Selected. При выборе ко- манды File-Exit программа завершается. Главное окно программы представлено объектом TMainWindow, в котором объявлен конструктор Init и метод ответа на сообщение cm_FileNew. Загрузку ресурса меню выполняет конструктор Init глав- ного окна, вызывая для этого функцию LoadMenu(Instance: THandle; MenuName: PChar): HMenu; где Instance - дескриптор копии приложения; MenuName - идентифи- катор меню в ресурсе. Функция возращает дескриптор меню. Иденти- фикатором ресурса может быть строка или целое число. Если это чис- ло, то при вызове функции его надо преобразовать к типу PChar (или к эквивалентному ему типу MakelntResource). Правильные указатели никогда не содержат в старшем слове нуль, поэтому функция загрузки ресурса всегда может определить, что ей передано - строка или число. В качестве идентификатора меню в программе MENU передается целочисленная константа idm_Menu, преобразованная к типу PChar. Дескриптор меню, возвращаемый функцией LoadMenu, сохраняется в поле Attr.Menu. Всего этого вполне достаточно для того, чтобы окно программы имело меню. При выборе в меню команды Windows посылает окну сообщение wm_Command. Библиотека OWL диспетчирует это сообщение окон- ному объекту, вызывая у него тот метод ответа на сообщение, селек- тор диапазона которого равен cm_First, а номер совпадает с иденти- фикатором команды меню. Для реакции на команду в оконном объек- те должен быть объявлен метод ответа на сообщение с соответствую- щим виртуальным индексом. Такой метод называется методом отве- та на команду. От других методов ответа на сообщения он отличается тем, что имеет селектор диапазона cmJFirst.
Гпава 5. Ресурсы Windows 165 В объекте TMainWindow определен лишь один метод ответа на команду - CMFileNew, он вызывается по команде cm_FileNew и вы- водит на экран сообщение "File-New" Command Selected с помощью функции MessageBox. В объекте TMainWindow не определена реакция на другие команды, однако при выборе в меню команды File-Exit ок- но программы все-таки реагирует на нее, завершая работу приложе- ния. Это объясняется тем, что объект TMainWindow наследует метод ответа на команду cmJExit от базового интерфейсного объекта TWindowsObject. По команде cm_Exit метод CMExit завершает рабо- ту прикладной программы. 5.4.4. ПОМЕТКА ПУНКТОВ МЕНЮ Слева от имени пункта всплывающего меню может располагаться символ-пометка в виде “птички”. Она обычно отражает состояние (включен-выключен) некоторого параметра программы, который регулируется командой меню. Помечаемые пункты меню служат той же цели, что и кнопки с зависимой и независимой фиксацией в диало- говых блоках. Однако переключение пометки меню приводит к его автоматическому закрытию, что создает некоторое неудобство, когда нужно включить или выключить несколько пунктов. Помечаемые пункты меню целесообразно использовать вместо кнопок только в том случае, когда необходимость в их переключении возникает доста- точно редко. В этом случае пользователь, как правило, готов мирить- ся с тем, что до них можно добраться только через меню. Кроме того, пользователю даже удобно, что лишние органы управления не отвле- кают его внимания при взаимодействии с другими элементами интер- фейса. Символ пометки можно устанавливать для любых пунктов меню, за исключением пунктов меню самого верхнего уровня. При разра- ботке выпадающего меню каждому из его пунктов можно указать, будет ли он помечен при первом же появлении меню. Чтобы сделать пункт меню помеченным изначально, достаточно в Resource Workshop включить для этого пункта в группе Initial state кнопку Checked. Устанавливает и снимает пометку пункта меню в программе функ- ция $ CheckMenuItem(Menu: HMenu; IDCheckltem, Check: Word): Bool; где Menu - дескриптор меню; IDCheckltem - пункт меню, для которо- го должна быть установлена или снята пометка; Check - указывает, как определяется позиция пункта меню, а также следует ли установить или снять пометку. Параметр Check содержит флаги, приведенные в табл. 5.1. Следует учитывать, что флаги mfJByCommand и
166 Программирование в среде Borland Pascal для Windows mf_ByPosition, а также mf_Checked и mf_Unchecked являются взаимо- исключающими. Функция CheckMenuItem возвращает предыдущее состояние пункта меню - mf_Checked или mf_Unchecked. Табл. 5.1. Флаги параметра Check для функции CheckMenuItem Константа Описание mf_ByCommand Указывает, что параметр IDCheckltem есть код команды меню. Флаг mf_ByCommand принимается по умолчанию mf_ByPosition Указывает, что параметр IDCheckltem есть номер пункта меню, начиная с нуля mf_Checked Пометка должна быть установлена mf_Unchecked Пометка должна быть снята Чтобы узнать, имеет ли пункт меню пометку, можно вызвать функцию GetMenuState. Вообще говоря, GetMenuState позволяет уз- нать еще и другую интересную информацию об элементе меню: что представляет собой элемент меню, разрешен он или запрещен и т.д. Функция GetMenuState объявлена в WINPROCS следующим образом: GetMenuState(HMenu; Menu: Id, Flags: Word): Word; где Menu - дескриптор меню; Id - элемент меню, состояние которого должно быть получено; Flags - определяет интерпретацию параметра Id и может принимать значение либо mf_ByCommand, либо mf_ByPosition. Если в параметре Flags передается значение mf_ByCommand, то параметр Id содержит код команды, а если mf_ByPosition - то позицию пункта меню, начиная с нуля. Функция GetMenuState возвращает флаги состояния для указанно- го пункта меню (табл. 5.2). Если указанный пункт в меню не существу- ет, функция возвращает 1. Если указанный в параметре Id пункт явля- ется всплывающим меню, то старший байт возвращаемого значения содержит число пунктов выпадающего меню. Младший байт всегда содержит состояние пункта меню, которое кодируется флагами из табл. 5.2. Пометка пунктов меню может осуществляться согласованно и не- согласованно. Например, в последнее время окна многих программ оформляются такими элементами пользовательского интерфейса, как инструментальная панель (Toolbar) и строка подсказки (Status Ваг). Для управления видимостью этих элементов окна в меню Options про- граммы обычно помещают пункты Toolbar и Status Ваг. Пользователь может независимо управлять видимостью инструментальной панели и строки подсказки, поэтому пометка команд Options-Toolbar и Options- Status Ваг должна тоже осуществляться независимо. Примером согла- сованной работы пунктов меню является работа команд Sort by Name,
Гпава 5. Ресурсы Windows 167 Sort by Type, Sort by Size и Sort by Date в меню View программы File Manager. Помеченным, или выбранным, может быть только один из четырех указанных пунктов. Табл. 5.2. Флаги состояния, возвращаемые функцией GetMenuState Константа Описание mf_Checked mf_Uncheckec! mfJEnabled mf_Disabled mf_Grayed mf_Separator Пункт меню помечен (только для выпадающих меню) Пункт меню не помечен Пункт меню разрешен (доступен) Пункт меню запрещен (недоступен) Пункт меню запрещен и тусклый Пункт меню представляет собой горизонтальную разделительную линию mf_MenuBreak Пункт меню начинает новую строку в горизонтальном меню и новый столбец в вертикальном mf_MenuBarBreak Аналог mf_MenuBreak, но в вертикальном меню между новой и старой колонками вставляется вертикальная разделительная линия mf_Bitmap Пункт меню является растровым изображением Программа CHECKS демонстрирует, как можно осуществлять управление включением и выключением пометок рядом с пунктами меню (рис. 5.4). Меню программы CHECKS было получено на основе меню про- граммы MENU добавлением всплывающего меню Options, состояще- го из пунктов Toolbar, Status Ваг и Sort by. Пункты Toolbar и Status Ваг работают как независимые переключатели. Выбор пункта Sort by Рис. 5.4. Окно программы CHECKS
168 Программирование в среде Borland Pascal для Windows приводит к появлению вложенного меню, состоящего из пунктов Name, Type, Size и Date, которые работают как зависимые переключа- тели. CHECKS.INC const idm_Menu = 100; cm_Toolbar =201; cm_StatusBar = 202; cm_SortByName = 203; cm_SortByType = 204; cm_SortBySize = 205; cm_SortByDate = 206; cm_About = 999; CHECKS.PAS program Checks; {$R CHECKS.RES} uses WinTypes, WinProcs, OWindows; {$1 CHECKS.INC} type PMainWindow = *TMainWindow; • TMainWindow = object(TWindow) SortOrder: Word; { код команды, помеченной в меню Options-Sort by} constructor lnit(AParent: PWindowsObject; ATitle: PChar); procedure CMToolbar(var Msg: Tmessage); virtual cm_First + cm_Toolbar; procedure CMStatusBar(var Msg: Tmessage); virtual cm_First + cm_StatusBar; procedure CMSortByName(var Msg: Tmessage); virtual cm_First + cm_SortByName; procedure CMSortByType(var Msg: Tmessage); virtual cm_First + cm_SortByType; procedure CMSortBySize(var Msg: Tmessage); virtual cm_First + cm_SortBySize; procedure CMSortByDate(var Msg: Tmessage); virtual cm_First + cm_SortByDate; procedure ToggleCheck(MenultemlD: Word); procedure ToggleSortOrder(MenultemlD: Word); end; PChecksApp = ATChecksApp; TChecksApp = object(TApplication) procedure InitMainWindow; virtual; end; { Конструирует оконный объект и загружает из ресурса меню } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); Attr.Menu := LoadMenu(Hlnstance, PChar(idm_Menu));
Глава 5. Ресурсы Windows 169 SortOrder := cm_SortByName; end; { Переключает пометку на пункте Toolbar меню Options } procedure TMainWindow,.CMToolbar(var Msg: TMessage); begin ToggleCheck(cm_Toolbar); end; { Переключает пометку на пункте Status Ваг меню Options } procedure TMainWindow.CMStatusBar(var Msg: TMessage); begin ToggleCheck(cm_StatusBar); end; { Устанавливает пометку в меню Options-Sort by на пункт Name } procedure TMainWindow.CMSortByName(var Msg: TMessage); begin ToggleSortOrder(cm_SortByName); end; { Устанавливает пометку в меню Options-Sort by на пункт Type } procedure TMainWindow.CMSortByType(var Msg: TMessage); begin ToggleSortOrder(cm_SortByType); end; { Устанавливает пометку в меню Options-Sort by на пункт Size } procedure TMainWindow.CMSortBySize(var Msg: TMessage); begin ToggleSortOrder(cm_SortBySize); end; { Устанавливает пометку в меню Options-Sort by на пункт Date } procedure TMainWindow.CMSortByDate(var Msg: TMessage); begin ToggleSortOrder(cm_SortByDate); end; { Переключает пометку заданного пункта меню } procedure TMainWindow.ToggleCheck(MenultemlD: Word); begin if GetMenuState(Attr.Menu, MenultemID, mf_ByCommand) and mf_Checked = 0 then CheckMenultem(Attr.Menu, MenultemID, mf_ByCommand or mf_Checked) else CheckMenultem(Attr.Menu, MenultemID, mf_ByCommand or mf_UnChecked); end; { Устанавливает пометку в меню Options-Sort by на заданной пункт } procedure TMainWindow.ToggleSortOrder(MenultemlD: Word); begin CheckMenultem(Attr.Menu, SortOrder, mf_ByCommand or mf_UnChecked); SortOrder := MenultemID; CheckMenultem(Attr.Menu, SortOrder, mf_ByCommand or mf_Checked); end; { Создает объект главного окна программы } procedure TChecksApp.lnitMainWindow;
170 Программирование в среде Borland Pascal для Windows begin MainWindow := New(PMainWindow, lnit(nil, ’Checkmarks in Menus’)); end; var ChecksApp: TChecksApp; begin ChecksApp. Init('Checks'); ChecksApp. Run; ChecksApp. Done; end. Установку и сброс метки рядом с пунктами Toolbar и Status Ваг выполняют методы CMToolbar и CMStatusBar. С этой целью они вызывают метод ToggleCheck, передавая ему код переключаемой в противоположное состояние команды: procedure TMainWindow.ToggleCheck(MenultemlD: Word); begin if GetMenuState( Attr.Menu, MenultemID, mf_ByCommand) and mf_Checked = 0 then CheckMenultem(Attr.Menu, MenultemID, mf_ByCommand or mf_Checked) else CheckMenultem(Attr.Menu, MenultemID, mf_ByCommand or mf_UnChecked); and; Метод ToggleCheck определяет состояние пункта меню и в зависи- мости от того, помечен он или нет, сбрасывает или устанавливает пометку с помощью функции CheckMenuItem. Пункты Name, Type, Size и Date вложенного меню Sort by работа- ют согласованно: пометка как бы перемещается между ними. При выборе одного из этих пунктов пометка снимается с предыдущего пункта и устанавливается на вновь выбранный. Обработку сообще- ний от названных пунктов выполняют методы CMSortByName, CMSortByType, CMSortBySize и CMSortByDate соответственно. Все они вызывают один и тот же метод ToggleSortOrder и передают ему код своего пункта меню. Метод ToggleSortOrder как раз и выполняет все необходимые действия: procedure TMainWindow.ToggleSortOrder(MenultemlD: Word); begin CheckMenultem(Attr.Menu, SortOrder, mf_ByCommand or mf_UnChecked); SortOrder := MenultemID; CheckMenultem(Attr.Menu, SortOrder, mf_ByCommand or mf_Checked); end; Алгоритм работы метода прост: снять пометку с текущей команды, код которой хранится в поле SortOrder, изменить значение SortOrder на новое, переданное в параметре MenultemID, и установить пометку на другую команду в соответствии с новым значением SortOrder.
Гпава 5. Ресурсы Windows 171 5.4.5. РАЗРЕШЕНИЕ И ЗАПРЕЩЕНИЕ ПУНКТОВ МЕНЮ Некоторые функции программы могут быть недоступны пользовате- лю в тот или иной момент времени. Пункты меню, соответствующие недоступным функциям, должны быть запрещены. Легче запретить выбор отдельных пунктов меню, чем программировать логику пове- дения на случай, когда пользователь выбрал неправильную команду. Например, когда в редакторе не открыт ни один файл, нужно запре- тить команды Save и Save as в меню File, поскольку в данной ситуации выбор этих команд является ошибкой. Для разрешения и запрещения пунктов меню служит функция EnableMenuItem(Menu: HMenu; IDEnableltem: Word; Enable: Word): Bool; где Menu - дескриптор меню; IDEnableltem - элемент меню, который устанавливается в состояние “разрешен” или “запрещен”; Enable - задает режим разрешения или запрещения и интерпретацию парамет- ра IDEnableltem. Значение параметра Enable может быть получено комбинацией двух констант: одной из множества {mf_Enabled, mf_Disabled, mf_Grayed} и одной из множества {mfJByCommand, mf_ByPosition}. Описание этих констант содержится в табл. 5.2. Если при вызове функции в параметре Enable указан флаг mf_ByCommand, то в параметре IDEnableltem передается код коман- ды меню, а если флаг mf_ByPosition, - то позиция элемента в пределах меню. При комбинировании одной из этих констант с mf_Enabled пункт меню становится разрешенным, с mf_Disabled - запрещенным, а с mf_Grayed - запрещенным и тусклым. Отметим, что в готовых про- граммах вы никогда не должны использовать флаг mfJDisabled для запрещения пункта меню, иначе пользователь не сможет визуально отличить запрещенную команду от разрешенной. Вместо него следует использовать флаг mf_Grayed. Флаг mf_Disabled целесообразно ис- пользовать только во время разработки и отладки программы. Обыч- но команду меню помечают флагом mfJDisabled, если реакция на нее в программе еще не реализована. Разрешение и запрещение пунктов меню демонстрирует программа GRAYMENU. В ней меню верхнего уровня состоит из единственного всплывающего меню File, которое содержит KOMaHAb&New, Open, Save, Save as, Close и Exit. Программа не выполняет никаких полезных дей- ствий, однако пункты меню File работают так, как будто открытие и закрытие файлов действительно происходит. При выборе команд New и Open команды Save, Save as и Close становятся доступными, а при выборе команды Close - недоступными. Причем для того чтобы пунк- ты Save и Save as стали недоступны, необходимо столько раз выбрать команду Close, сколько раз до этого были выбраны команды New и
172 Программирование в среде Borland Pascal для Windows Open. Такая особенность как раз имеет место в приложениях с много- оконным интерфейсом (об этом мы поговорим в гл. 7). GRAYMENU.INC const idm_Menu = 100; cm_FileClose = 205; GRAYMENU.PAS program GrayMenu; {$R GRAYMENU.RES} uses WinTypes, WinProcs, OWindows; {$1 GRAYMENU.INC} type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) WndCount: Word; { счетчик открытых окон } constructor lnit(AParent: PWindowsObject; ATitle: PChar); procedure CMFileNew(var Msg: TMessage); virtual cm_First + cm_FileNew; procedure CMFileOpen(var Msg: TMessage); virtual cm_First + cm_FileOpen; procedure CMFileClose(var Msg: TMessage); virtual cm_First + cm_FileClose; procedure UpdateMenu; virtual; end; PGrayMenuApp = ATGrayMenuApp; TGrayMenuApp = object(TApplication) procedure InitMainWindow; virtual; function IdleAction: Boolean; virtual; end; { Конструирует оконный объект и загружает из ресурса меню } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); Attr.Menu := LoadMenu(HInstance, PChar(idm_Menu)); WndCount := 0; end; { По команде меню File-New увеличивает счетчик открытых окон } procedure TMainWindow.CMFileNew(var Msg: TMessage); begin Inc(WndCount); end; { По команде меню File-Open увеличивает счетчик открытых окон } procedure TMainWindow.CMFileOpen(var Msg: TMessage); begin Inc(WndCount); end; { По команде меню File-Close уменьшает счетчик открытых окон } procedure TMainWindow.CMFileClose(var Msg: TMessage);
Глава 5. Ресурсы Windows 173 begin Dec(WndCount); end; { Управляет разрешением и запрещением команд меню } procedure TMainWindow.UpdateMenu; var State: Word; begin if WndCount = 0 then State := mf_ByCommand or mf_Grayed else State := mf_ByCommand or npf_Enabled; EnableMenultem(Attr.Menu, cm_FileSave, State); EnableMenultem(Attr.Menu, cm_FileSaveAs, State); EnableMenultem(Attr.Menu, cm_FileClose, State); end; { Создает объект главного окна программы } procedure TGrayMenuApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Graying Menu Items’)); end; { Обновляет меню в холостом цикле программы } function TGrayMenuApp.ldleAction; begin IdleAction := False; PMainWindow(MainWindow)A.UpdateMenu; end; var GrayMenuApp: TGrayMenuApp; begin GrayMenuApp.lnit('GrayMenu'); GrayMenuApp.Run; GrayMenuApp. Done; end. Для контроля за числом потенциально открытых документов в объекте главного окна программы (TMainWindow) объявлено поле WndCount. Оно инициализируется нулем в конструкторе окна и мо- дифицируется методами CMFileNew, CMFileOpen и CMFileClose. В полноценных приложениях число команд, которые требуется то запрещать, то разрешать, может быть достаточнее велико, поэтому желательно, чтобы управление командами меню было сосредоточено в одном месте. Конечно, методы ответа на команды можно расширить дополнительными функциями, которые будут разрешать или запре- щать другие, зависящие от них команды. Однако существует более элегантное решение данной задачи. Оно основано на использовании холостого цикла работы программы.
174 Программирование в среде Borland Pascal для Windows В объекте TApplication существует виртуальный метод IdleAction, который предоставляет приложению возможность выполнять фоно- вую работу. Он вызывается из цикла обработки сообщений всякий раз, когда очередь приложения становится пустой. Вы можете пере- крыть метод IdleAction в наследнике TApplication, поместив в него любой программный код. Необходимо помнить, что действия, вы- полняемые в IdleAction, обязательно должны быть короткими, при этом они могут представлять собой либо отдельные завершенные операции, либо последовательные шаги некоторой длительной опера- ции. Метод IdleAction возвращает значение типа Boolean. Если возвра- щается True, то IdleAction вызывается до тех пор, пока в очереди при- ложения не появится сообщение, а если False, - то IdleAction будет вызван в следующий раз только при том условии, что в очереди при- ложения сначала появятся, а потом снова исчезнут сообщения. И в том, и в другом случае IdleAction вызывается при отсутствии сообще- ний в очереди приложения, но в первом случае фоновая обработка занимает весь промежуток времени, пока прикладная очередь остается пустой. По умолчанию IdleAction просто возвращает False. В программе GRAYMENU объявлен объект TGrayMenuApp (наследник TApplication), в котором перекрыт виртуальный метод IdleAction: function TGrayMenuApp.ldleAction; begin IdleAction := False; PMainWindow(MainWindow)A.UpdateMenu; end; Метод TGrayMenuApp.ldleAction вызывает у объекта главного окна MainWindow метод UpdateMenu, который непосредственно управляет разрешением и запрещением команд cm_FileSave, cm_FileSaveAs и cm_FileClose: procedure TMainWindow.UpdateMenu; var State: Word; begin if WndCount = 0 then State := mf_ByCommand or mf_Grayed else State := mf_ByCommand or mf_Enabled; Enable Men u I tem(Attr. Menu, cm_FileSave, State); EnableMenultem(Attr.Menu, cm_FileSaveAs, State); EnableMenultem(Attr.Menu, cm_FileClose, State); end;
Гпава 5. Ресурсы Windows 175 Методы ответа на команды меню cm_FileNew, cm_FileOpen и cm_FileClose только увеличивают или уменьшают на единицу счетчик потенциально открытых окон WndCount. Таким образом, логика управления командами меню в программе GRAYMENU отделена от действий, выполняемых методами ответа на команды. Программа, построенная по такому принципу, более понятна, ее легче отлаживать, изменять и расширять. 5.4.6. ИЗМЕНЕНИЕ СИСТЕМНОГО МЕНЮ Кроме загрузки меню из ресурса, существует возможность динамиче- ского создания меню. Вызывая предназначенные для этой цели функ- ции Windows, вы можете создавать меню любой сложности с помо- щью операторов программы. Чаще меню загружают из ресурса, так как это проще и эффективнее, поэтому мы не будем останавливаться на функциях динамического создания меню подробно; в случае необ- ходимости сведения о них вы найдете в руководстве по Borland Pascal. Рассмотрим лишь один случай, требующий изменения числа элемен- тов меню программным путем, - добавление новой команды в сис- темное меню окна. Вообще говоря, системное меню приходится изменять редко, так как оно имеет другое назначение, нежели обычное меню программы. Его создает и контролирует система Windows, программисту об этом беспокоиться не надо. Тем не менее в системное меню бывает удобно добавить некоторую команду, имеющую особый смысл. Например, в некоторых программах в системное меню добавляют команду Always on Тор, при выборе которой Windows начинает поддерживать распо- ложение данного окна поверх всех остальных таким образом, что пользователь всегда видит его на экране, даже когда оно неактивно или минимизировано. При повторном выборе команды Always on Top окно теряет указанное свойство. Пункт Always on Тор может иметь пометку, которая указывает, наделено окно свойством располагаться поверх остальных или нет. Разрабатывая такую программу, необхо- димо уметь добавить в системное меню окна новый пункт и обеспе- чить в программе прием и обработку сообщений, генерируемых этим пунктом. Прежде, чем добавить новый пункт, нужно получить дескриптор системного меню. Сделать это позволяет функция * GetSystemMemi(Wnd: HWnd; bRevert: Bool): HMenu; где Wnd - дескриптор окна (поле HWindow, если функцию вызывает оконный объект); bRevert - определяет, какое из двух действий следует выполнить. Если параметр bRevert равен False, то функция создает копию системного меню, назначает эту копию окну и возвращает ее
176 Программирование в среде Borland Pascal для Windows дескриптор. Полученный дескриптор может использоваться для мо- дификации системного меню окна. Если значение bRevert равно True, функция уничтожает существующую в окне копию системного меню и восстанавливает стандартное системное меню. В этом случае возвра- щаемое значение не определено. Чтобы добавить в системное меню пункт Always on Тор, нужно вы- полнить следующие действия: HSysMenu := GetSystemMenu(HWindow, False); AppendMenu(HSysMenu, mf_String, cm_AlwaysOnTop, Always on &Top’); Добавление нового пункта обеспечивает функция AppendMenu, которой передаются: дескриптор системного меню HSysMenu; флаг mf_String, показывающий, что пункт меню представляет собой стро- ку; закрепляемая за новым пунктом команда cm_AlwaysOnTop и тек- стовая строка с названием пункта. Как мы уже отмечали, для реакции на команду обычного меню в наследнике TWindow достаточно объявить динамический метод отве- та на сообщение с нужным виртуальным индексом. Однако реакция на команды системного меню реализуется иначе. Каждый раз при выборе пункта системного меню Windows посылает приложению со- общение wm_SysCommand. Параметр WParam этого сообщения со- держит идентификатор выбранной команды. Для реакции на команды системного меню в наследнике TWindow необходимо объявить дина- мический метод WMSysCommand с индексом диспетчирования, рав- ным wm_First + wm_SysCommand: TMainWindow = object(TWindow) procedure CMAIwaysOnTop(var Msg: TMessage); procedure WMSysCommand(var Msg: TMessage); virtual wm_First + wm_SysCommand; end; Кроме динамического метода WMSysCommand, мы объявили в объекте TMainWindow еще метод CMAlwaysOnTop. Это обычный статический метод, который осуществляет все необходимые действия по команде cm_AlwaysOnTop. Он вызывается из динамического мето- да WMSysCommand, когда код команды меню равен cm_AlwaysOnTop. В принципе, метод CMAlwaysOnTop можно было бы и не объявлять, но тогда всю “логику” обработки команды cm_AlwaysOnTop пришлось бы вставить в WMSysCommand. Метод WMSysCommand в нашем примере выглядит так: procedure TMainWindow.WMSysCommand(var Msg: TMessage); begin case Msg.WParam of cm_AlwaysOnTop: CMAIwaysOnTop(Msg);
Глава 5. Ресурсы Windows 177 else inherited WMSysCommand(Msg); end; end; В случае, когда значение поля WParam записи Msg равно cm_AlwaysOnTop, вызывается метод CMAlwaysOnTop. В противном случае WMSysCommand вызывает одноименный метод предка. Чтобы окно всегда располагалось над другими окнами, в методе CMAlwaysOnTop вызывается функция SetWindowPos, в которую пер- вым параметром передается дескриптор окна, вторым - константа hwnd_TopMost, а последним - выражение swp_NoMove or swp_NoSize. Остальные параметры в данном случае могут иметь про- извольное значение. При втором вызове CMAlwaysOnTop окно теряет способность располагаться поверх остальных. Для этого вторым па- раметром функции SetWindowPos передается константа hwnd_NoTopMost. Функция SetWindowPos является многоцелевой. Мы не будем утомлять вас длительным описанием всех ее возможно- стей и надеемся, что при необходимости вы сможете разобраться с этим самостоятельно (с помощью справочного руководства). Ниже приводится программа SYSMENU, в которой показано, как выполняется расширение системного меню командой Always on Top: program SysMenu; uses WinTypes, WinProcs {Sifdef Ver80}, Messages {Seise}, Win31 {Sendif}, OWindows; const cm_AlwaysOnTop = 100; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) HSysMenu: HMenu; { дескриптор системного меню} procedure SetupWindow; virtual; procedure CMAIwaysOnTop(var Msg: TMessage); procedure WMSysCommand(var Msg: TMessage); virtual wm_First + wm_SysCommand; end; PSysMenuApp = ATSysMenuApp; TSysMenuApp = object(TApplication) procedure InitMainWindow; virtual; end; { После создания окна добавляет в системное меню новы# пункт } procedure TMainWindow.SetupWindow; begin inherited SetupWindow; HSysMenu := GetSystemMenu(HWindow, False); AppendMenu(HSysMenu, mf_Separator, 0, nil); AppendMenu(HSysMenu, mf_String, cm_AlwaysOnTop, 'Always on &Top’); end;
178 Программирование в среде Borland Pascal для Windows { Управляет свойством окна располагаться поверх остальных окон } procedure TMainWindow.CMAIwaysOnTop(var Msg: TMessage); begin if GetMenuState(HSysMenu, cm__AlwaysOnTop, mf_ByCommand) and mf_Checked = 0 then begin SetWindowPos(HWindow, hwnd_TopMost, 0, 0, 0, 0, swp_NoMove or swp_NoSize); CheckMenultem(HSysMenu, cm_AlwaysOnTop, mf_ByCommand or mf_Checked); end else begin SetWindowPos(HWindow, hwnd_NoTopMost, 0, 0, 0, 0, swp_NoMove or swp_NoSize); CheckMenultem(HSysMenu, cm__AlwaysOnTop, mf_ByCommand or mfJJnChecked); end; end; { Обрабатывает команды системного меню } procedure TMainWindow.WMSysCommand(var Msg: TMessage); begin case Msg.WParam of cm_AI waysOnTop: С M Al way sOnT op( M sg); else inherited WMSysCommand(Msg); end; end; { Создает объект главного окна программы } procedure TSysMenuApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’System Menu Demo')); end; var SysMenuApp: TSysMenuApp; begin SysMenuApp. Init(’SysMenu'); SysMenuApp. Run; SysMenuApp. Done; end. 5.4.7. ЛОКАЛЬНОЕ МЕНЮ Если при работе в Borland Pascal в окне редактора нажать правую кнопку мыши, то в том месте, где находился указатель мыши, появит- ся локальное меню. Локальные меню не имеют постоянного места внутри окна и могут появляться в любом месте экрана. Они появляются (“всплывают”) лишь по специальному требованию со стороны пользователя (например, нажатие правой кнопки мыши). Доступ к командам ло-
Гпава 5. Ресурсы Windows 179 кального меню происходит быстрее, чем к командам обычного меню, поэтому использование локальных меню делает пользовательский интерфейс более удобным. Локальные меню идеально подходят для размещения команд, специфичных для каждого отдельного окна. Локальное меню может быть создано и наполнено элементами: PopupMenu := CreatePopupMenu; AppendMenu(PopupMenu, mf_String, cmjteml, Item &1 ’); AppendMenu(PopupMenu, mf_String, cmjtem2, 'Item &2'); AppendMenu(PopupMenu, mf_Separator, 0, nil); AppendMenu(PopupMenu, mf_String, cm_Exit, 'E&xit'); Для создания локального меню в первой строке вызывается функ- ция CreatePopupMenu. Полученный дескриптор меню сохраняется в переменной PopupMenu, имеющей тип HMenu. Далее с помощью по- следовательных обращений к функции AppendMenu меню наполняет- ся элементами. Следующим шагом является отображение меню и предоставление пользователю возможности выбора одной из его команд. Для этого служит функция TrackPopupMenu(Menu: HMenu; Flags: Word; X, Y, Reserved 1: Integer; Wnd: HWnd; Reserved!: Pointer): Bool; где Menu - дескриптор локального меню; Flags - флаги, определяю- щие горизонтальное выравнивание меню и способ* выбора команды (табл. 5.3); X - горизонтальная позиция меню на экране (зависит от значения параметра Flags); Y - вертикальная позиция верхнего края меню на экране; Reservedl - зарезервирован и должен быть равен ну- лю; Wnd - окно, которое получит сообщение wm_Command после выхода из функции TrackPopupMenu; Reserved2 - указатель на запись типа TRect, содержащую экранные координаты прямоугольника, в пределах которого щелчки мыши не вызывают пропадания меню. Если значение параметра Reserved! равно nil, то координаты этого прямоугольника считаются равными координатам самого меню. Табл. 5.3. Флаги функции TrackPopupMenu Константа Описание tpm_CenterAlign Меню центрируется горизонтально относительно коор- динаты, переданной в параметре X tpm_LeftAlign Параметр X указывает положение левого края меню tpm_RightAlign Параметр X указывает положение правого края меню tpm_LeftButton Выбор команды осуществляется левой кнопкой мыши tpm_RightButton Выбор команды осуществляется правой кнопкой мыши
180 Программирование в среде Borland Pascal для Windows Используя функцию TrackPopupMenu, можно отобразить и запус- тить созданное выше меню: TrackPopupMenu(PopupMenu, tpm_LeftAlign or tpm_RightButton, P.X, P.Y, 0, HWindow, nil); DestroyMenu(PopupMenu); Предполагается, что отображение и запуск меню выполняет окон- ный объект, поэтому в качестве параметра Wnd в TrackPopupMenu передается элемент данных HWindow. Меню PopupMenu отображает- ся на экране в позиции (200, 150), где 200 - координата левой границы окна меню, что определяется флагом tpm_LeftAlign. Для того чтобы выбор команды меню мог осуществляться правой кнопкой мыши, вместе с флагом tpm_LeftAlign передается флаг tpm_RightButton. По- сле выбора пользователем команды меню функция TrackPopupMenu завершается, а в очередь приложения помещается сообщение wm_Command, адресованное окну HWindow и содержащее код вы- бранной команды. В заключение функция DestroyMenu разрушает всплывающее меню в памяти. Ниже приводится текст программы POPUP, демонстрирующей ра- боту всплывающего меню по нажатии правой кнопки мыши: program Popup; uses WinTypes, WinProcs {SifdefVer80}, Messages {Seise}, Win31 {Sendif}, OWindows; const cmjteml = 101; cm_ltem2 = 102; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) procedure CMItem1(var Msg: TMessage); virtual cm_First + cmjteml; procedure CMItem2(var Msg: TMessage); virtual cm_First + cmjtem2; procedure WMRButtonDown(var Msg: TMessage); virtual wm_First + wm_RButtonDown; end; PPopupApp = ATPopupApp; TPopupApp = object(TApplication) procedure InitMainWindow; virtual; end; { По команде Item 1 локального меню выдает на экран сообщение } procedure TMainWindow.CMItem1(var Msg: TMessage); begin MessageBox(HWindow, ’"Item Г Selected', ’Message', mb_Ok); end; { По команде Item 2 локального меню выдает на экран сообщение } procedure TMainWindow.CMItem2(var Msg: TMessage);
Гпава 5. Ресурсы Windows 181 begin MessageBox(HWindow, ’’’Item 2" Selected', 'Message', mb_Ok); end; { При нажатии правой кнопки мыши открывает локальное меню } procedure TMainWindow.WMRButtonDown(var Msg: TMessage); var PopupMenu: HMenu; P: TPoint; begin PopupMenu := CreatePopupMenu; AppendMenu(PopupMenu, mf_String, cmjteml, 'Item &T); AppendMenu(PopupMenu, mf_String, cmjtem2, 'Item &2'); AppendMenu(PopupMenu, mf_Separator, 0, nil); AppendMenu(PopupMenu, mf_String, cm_Exit, 'E&xit'); P.X := Msg.LParamLo; ’ P.Y := Msg.LParamHi; ClientToScreen(HWindow, P); TrackPopupMenu(PopupMenu, tpm_LeftAlign or tpm_RightButton, P.X, P.Y, 0, HWindow, nil); DestroyMenu(PopupMenu); end; { Создает объект главного окна программы } procedure TPopupApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'Popup Menu Demo’)); end; var PopupApp: TPopupApp; begin PopupApp. Init('Popup'); PopupApp. Run; PopupApp.Done; end. 5.5. АКСЕЛЕРАТОРЫ 5.5.1. ОБЩИЕ СВЕДЕНИЯ Обычные “горячие клавиши” (например, Fl, F3, Alt+X) в Windows принято называть акселераторными клавишами или просто акселера- торами. Они используются для ускорения ввода команд в программу со стороны пользователя. Часто акселераторные клавиши дублируют команды меню. Например, вы можете открыть файл в Borland Pascal не только выбором команды Open... из меню File, но и с помощью “горячей” клавиши F3, что гораздо быстрее (для этого в Options- Environment-Preferences... должно быть установлено множество команд Alternate, а не CUA). Клавиша F3 является акселераторной. Это зна- чит, что ее нажатие Windows преобразует в команду, в данном случае
182 Программирование в среде Borland Pascal для Windows в команду открытия файла. Акселератором может быть и комбинация клавиш, например Alt+X. Нужно иметь в виду, что не существует стандартного закрепления акселераторных клавиш за командами меню. Например, в Windows “горячей” клавишей открытия файла является Ctrl+Fl 2, в то время, как в Borland Pascal - F3. Существует всего несколько общепринятых закреплений клавиш: F1 за командой Help, Shift+Del за командой Cut из меню Edit, Ctrl+Ins за командой Edit-Сору, Shift+Ins За Edit-Paste. Отметим, *что команды, генерируемые акселераторными клавиша- ми, ничем не отличаются от команд, генерируемых пунктами меню. В то же время, акселераторные клавиши никак не связаны с меню. Вы можете создать программу, в которой вообще не будет меню, а все команды будут генерироваться с помощью акселераторных клавиш. Однако такой подход к построению пользовательского интерфейса не отвечает стандарту CUA (Common User Access). Поэтому мы советуем вам для каждой акселераторной клавиши иметь в меню пункт с такой же командой. В ресурсе акселераторные клавиши хранятся в виде таблицы и должны загружаться в программу при ее запуске. В одном файле ре- сурса может быть несколько таблиц акселераторных клавиш. Однако в OWL-программе в каждый момент времени может использоваться только одна из них. Рассмотрим, как создается в Resource Workshop таблица акселера- торов. 5.5.2. СОЗДАНИЕ ТАБЛИЦЫ АКСЕЛЕРАТОРОВ Для того чтобы перейти к определению акселераторных клавиш, вы- бираем в меню Resource команду New и в появившемся диалоговом окне указываем тип ресурса ACCELERATORS. В проект добавляется таблица акселераторов и в главном окне Resource Workshop открыва- ется окно редактора (рис. 5.5). В левой части окна находятся диалоговые элементы для ввода па- раметров текущей акселераторной клавиши, а в правой части - список созданных акселераторов. Для того чтобы добавить в список новый элемент, нужно в меню Accelerator выбрать команду New item или в окне нажать клавишу Ins. Перемещение между элементами таблицы осуществляется с помощью клавиш Ctrl+T и Ctrl+4< или мышью. Для того чтобы определить акселераторную клавишу, нужно в ле- вой части окна ввести следующие параметры: Command - команда, генерируемая акселераторной клавишей. В этом поле может указываться целое число или идентификатор, зареги- стрированный в списке идентификаторов Resource Workshop. Напри- мер, если в меню существует команда открытия файла с идентифика-
Глава 5. Ресурсы Windows 183 тором cmJFileOpen, то для определения акселератора с такой ко- мандой наберите в этом поле cmJFileOpen; Key - клавиша, нажатие которой генерирует команду, задаваемую в поле Command. Существуют определенные правила ввода клавиши в поле Key, связанные с тем, что она может совпадать с акселераторной клавишей самого редактора Resource Workshop. Для ввода параметра Key активизируйте с помощью мыши строку ввода Command и на- жмите клавишу Tab. После этого нажмите на клавиатуре клавишу (можно в комбинации с Alt, Shift или Ctrl), с которой вы желаете свя- зать заданную в поле Command команду. Resource Workshop распо- знает нажатую комбинацию клавиш и сам поместит в поле Key соот- ветствующее значение. Чтобы выйти из режима распознавания кла- виши, нажмите Alt+Esc или щелкните один раз мышью; Key type - тип клавиши: символьная (Ascii) или виртуальная (Virtual key). Символьные клавиши при нажатии генерируют символ, например а, Ь, х и т.д. Они редко используются в качестве акселера- торных, поскольку пользователь теряет возможность ввода обычных символов (букв и цифр). Однако комбинация символьной клавиши с Alt вполне может служить акселератором. Например, для быстрого выхода из Borland Pascal используется комбинация Alt+X. Чаще все- Рис. 5.5. Окно редактора таблицы акселераторов
184 Программирование в среде Borland Pascal для Windows го, в качестве акселераторов используются несимвольные клавиши. При назначении акселератора на несимвольную клавишу нужно учиты- вать, что Windows виртуализирует клавиатурный ввод. Это значит, что в программу поступают не абсолютные коды клавиш, полученные из порта клавиатуры, а коды системы Windows. Их называют вирту- альными кодами. Виртуализация клавиатурного ввода способствует независимости программного обеспечения от аппаратных особенно- стей компьютера (в данном случае клавиатуры), а это повышает его мобильность. Виртуальные коды Windows начинаются с символов vk_ и объявлены в модуле WINTYPES (см. приложение). Например, кла- виша F3 имеет виртуальный код vk_F3, где vk_F3 - идентификатор соответствующей целочисленной константы. Если при определении значения параметра Key нажимается несимвольная клавиша, то аксе- лератор назначается на виртуальную клавишу; Modifiers - модификаторы. К ним относятся клавиши Alt, Shift, Ctrl, используемые совместно с другими клавишами. Можно исполь- зовать любую комбинацию Alt, Shift, Ctrl. Модификаторы Alt, Shift, Control выставляются автоматически при определении клавиши в поле Key. Если некоторую команду меню можно выполнить с помощью ак- селератора, это должно быть отражено в названии пункта меню. На- пример, в меню File напротив пункта Open... стоит F3. Для того чтобы добавить к пункту Open... метку F3, нужно при определении этого пункта меню записать в поле Item text строку: &Open\tF3 где \t является управляющей последовательностью и соответствует символу табуляции. Символ табуляции в названии пункта указывает на то, что следующий за ним текст должен быть прижат к правому краю окна меню. Используя приведенную выше методику, мы добавили к меню из файла MENU.RC таблицу акселераторов и получили новый тексто- вый файл описания ресурса ACCEL.RC и готовый к подключению двоичный ACCEL.RES. В созданной нами таблице акселераторов определена единственная “горячая” клавиша F3. За ней закреплена команда cmJFileOpen. Таблица акселераторов из ресурса ACCEL.RC приведена на рис. 5.5. 5.5.3. ПОДКЛЮЧЕНИЕ АКСЕЛЕРАТОРОВ Для того чтобы в программе на OWL выполнялась обработка акселе- раторных клавиш, нужно в объекте TApplication загрузить из ресурса таблицу акселераторов и присвоить ее дескриптор элементу данных НАссТаЫе. Цикл обработки сообщений в объекте TApplication (метод
Гпава 5. Ресурсы Windows 185 MessageLoop) устроен таким образом, что после извлечения сообще- ния из очереди выполняется попытка обработать его как сообщение акселераторной клавиши. Это делается только в том случае, когда таблица акселераторов загружена, т.е. дескриптор HAccTable не равен нулю. Загрузку таблицы выполняет функция LoadAccelerators, которая объявлена в WINPROCS следующим образом: LoadAccelerators(Instance: THandle; TableName: PChar): THandle; где Instance - экземпляр приложения, загружающий таблицу акселера- торов (в этом параметре нужно передавать дескриптор HInstance, объявленный в модуле SYSTEM); TableName - акселераторная табли- ца в ресурсе программы. Идентификатором ресурса может быть стро- ка или целое число. Если это число, то при вызове LoadAccelerators его надо преобразовать к типу MakelntResource или PChar. Функция LoadAccelerators возвращает дескриптор таблицы акселераторов в памяти. Загрузку акселераторов логичнее всего производить при инициа- лизации экземпляра приложения, т.е. в методе Initlnstance прикладно- го объекта. Для этого объявляется наследник объекта TApplication, например объект TAccelApp: TAccelApp = object(TApplication) procedure Initlnstance; virtual; procedure InitMainWindow; virtual; end; Метод InitMainWindow всегда определяется в прикладном объекте для создания объекта главного окна. Мы уже знаем, как он должен выглядеть, поэтому приведем только реализацию Initlnstance: procedure TAccelApp. Initlnstance; begin inherited Initlnstance; HAccTable := LoadAccelerators(HInstance, PChar(ida_AccTable)); end; Здесь константа ida_AccTable задает идентификатор акселераторной таблицы в ресурсе. Предполагается, что она объявлена выше. Результатом нажатия акселераторной клавиши является командное сообщение, которое поступает в главное окно программы. Метод ответа на команду акселератора объявляется в оконном объекте точно так же, как метод ответа на команду меню. В программе ACCEL ко- манда открытия файла cmJFileOpen генерируется не только пунктом File-Open, но и клавишей F3. Ее обработку выполняет один и тот же метод CMFileOpen объекта TMainWindow:
186 Программирование в среде Borland Pascal для Windows TMainWindow = object(TWindow) constructor lnit(AParent: PWindowsObject; ATitle: PChar); procedure CMFileOpen(var Msg: TMessage); virtual cm_First + cm_FileOpen; end; Конструктор Init перекрыт в объекте TMainWindow с целью загрузки меню из ресурса. Метод CMFileOpen вызывается при выборе пункта меню File-Open или по нажатии клавиши F3. Приведем полный текст программы ACCEL, демонстрирующей подключение и использование таблицы акселераторов: ACCEL.INC const cm_About = 999; idm_Menu = 100; ida_AccTable = 100; ACCEL.PAS program Accel; {$R ACCEL.RES} uses WinTypes, WinProcs, OWindows, Strings; {$1 ACCEL INC} type ' PMainWindow = ATMainWindow; TMainWindow = object(TWindow) constructor lnit(AParent: PWindowsObject; ATitle: PChar); procedure CMFileOpen(var Msg: TMessage); virtual cm_First + crn_FileOpen; end; PAccelApp = ATAccelApp; TAccelApp = object(TApplication) procedure Initlnstance; virtual; procedure InitMainWindow; virtual; end; { Конструирует оконный объект и загружает из ресурса меню } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); Attr.Menu := LoadMenu(Hlnstance, PChar(idm_Menu)); end; { По команде меню File-New выдает на экран сообщение } procedure TMainWindow.CMFileOpen(var Msg: TMessage); begin MessageBox(HWindow, ’"File:Open" Command Selected or "F3" pressed’, ’Message’, mb_Ok); end; { При инициализации загружает из ресурса таблицу акселераторов } procedure TAccelApp.lnitlnstance; begin
Гпава 5. Ресурсы Windows 187 inherited Initlnstance; HAccTable := LoadAccelerators(HInstance, PChar(ida_AccTable)); end; { Создает объект главного окна программы } procedure TAccelApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'Accelerator Demo')); end; var AccelApp: TAccelApp; begin AccelApp. Init('Accel'); AccelApp.Run; AccelApp. Done; end. 5.6. ПИКТОГРАММЫ 5.6.1. НАЗНАЧЕНИЕ ПИКТОГРАММ Пиктограмма (icon) - это небольшая картинка, “иконка”, графиче- ский образ, который в Windows соответствует некоторой программе или окну. Пиктограммы значительно облегчают взаимодействие пользовате- ля с Windows, особенно неподготовленного. Так, в среде Program Manager каждой программе ставится в соответствие пиктограмма, которая содержит интуитивно понятную пользователю информацию о том, для чего эта программа предназначена, и служит для ее запуска. Например, программа Notepad имеет пиктограмму, на которой изо- бражена записная книжка, и запускается по двухкратному щелчку мыши. Другое применение пиктограмм - отображение окна в’ мини- мизированном состоянии. При выборе в программном окне кнопки минимизации это приложение сворачивается в пиктограмму, которая отображается внизу экрана. Чтобы восстановить приложение, нужно выбрать его пиктограмму. Пиктограмма может рассматриваться как частный случай более общего типа изображений, называемых растровыми (создание и ис- пользование растровых изображений рассмотрено в 5.8). В Windows растровое изображение всегда является прямоугольным. Однако, если вы заметили, пиктограммы могут иметь произвольную форму. Как же этого достичь? Решение состоит в том, что для каждой пиктограммы хранится одновременно два битовых образа: AN D-маска и XOR- маска. AND-маска является монохромной, XOR-маска - монохром- ной или цветной. Как правило, AND-маска является инверсным бито- вым образом того изображения, которое кодирует XOR-маска. При
188 Программирование в среде Borland Pascal для Windows выводе пиктограммы Windows комбинирует эти маски с изображени- ем на экране по следующему правилу: экран := (экран and AND-маска) хог XOR-маска Накладывая AND-маску, Windows сначала “вырезает” на экране пустую область с заданным контуром. После этого с помощью опера- ции XOR выводится XOR-маска, которая содержит непосредственно изображение пиктограммы. В результате создается впечатление, что пиктограмма имеет форму изображения. Этот эффект используется в динамических играх при перемещении спрайтов - небольших под- вижных изображений. Пиктограмма может иметь один из стандартных форматов, приве- денных в табл. 5.4. Размеры и цветовая гамма пиктограммы зависят от возможностей аппаратуры, т.е. от типа графического адаптера. В принципе, можно создать одну пиктограмму наибольших размеров и с широкой цветовой палитрой. При использовании адаптера, обла- дающего недостаточным разрешением и количеством цветов, Windows сама масштабирует пиктограмму и подбирает из доступной палитры цвета, наиболее близкие к оригинальным. Однако такая ап- проксимация часто искажает изображение, поэтому на практике при- ходится создавать отдельную пиктограмму для каждого типа графи- ческого адаптера. Табл, 5.4. Форматы пиктограмм Количество цветов Разрешение 32x16 Разрешение 32x32 Разрешение 64 x 64 2 EGA, monoVGA, VGA, SuperVGA EGA, monoVGA, VGA, SuperVGA EGA, monoVGA, VGA, SuperVGA 8 EGA, VGA, SuperVGA EGA, VGA, SuperVGA EGA, VGA, SuperVGA 16 EGA, VGA, SuperVGA EGA, VGA, SuperVGA EGA, VGA, SuperVGA 256 SuperVGA SuperVGA SuperVGA Разрабатываемые ресурсы пиктограмм хранятся в виде отдельных файлов с расширением ICO или в одном RES-файле вместе с другими ресурсами. На этапе компоновки прикладной программы они встраи- ваются в исполняемый ЕХЕ-модуль, из которого могут загружаться стандартными функциями Windows. И в ICO-, и в RES-файле может храниться одновременно несколько пиктограмм различного формата, которые Windows воспринимает как одну пиктограмму. В этом случае идентификатор присваивается не каждой отдельной пиктограмме, а всей группе. При загрузке такой
Глава 5. Ресурсы Windows 189 “пиктограммы” из ресурса Windows выбирает тот формат, который наиболее подходит для текущего графического режима. Коммерческие приложения должны иметь как можно больше форматов для каждой пиктограммы, чтобы их можно было использовать на различных гра- фических адаптерах. Самым общеупотребительным является формат 32x32 точки 16 цветов. 5.6.2. РАЗРАБОТКА ПИКТОГРАММ Пиктограммы могут создаваться как отдельно, так и в рамках неко- торого проекта. Создавая пиктограмму в рамках проекта, мы выби- раем в меню Resource команду New и в появившемся диалоговом окне указываем тип ресурса ICON. После этого Resource Workshop предла- гает определить форму хранения пиктограммы: в виде исходного тек- ста в RC-файле или в двоичном формате в виде отдельного ICO- файла. Хранение пиктограммы в виде ICO-файла позволяет использо- вать ее в других проектах и программах. Этот способ более удобный, поэтому его мы и выберем. В ответ-на наш выбор Resource Workshop предлагает ввести имя файла, в который пиктограмма будет помеще- на. Когда все это сделано и новый ICO-файл создан, на экран выво- дится диалоговое окно, в котором задается формат пиктограммы. Выбираем формат 32x32 точки 16 цветов и нажимаем кнопку Ок. Ме- ню Resource Workshop расширяется новыми пунктами и на экране появляется окно редактора пиктограмм (рис. 5.6). Рис. 5.6. Окно редактора пиктограмм
190 Программирование в среде Borland Pascal для Windows Для удобства работы окно состоит из двух панелей. На одной из них пиктограмма имеет оригинальный размер, на другой она увели- чена. Вы можете рисовать на любой из панелей, причем изменения, сделанные на* одной, отображаются и на другой. Оба изображения можно независимо друг от друга увеличивать и уменьшать командами View-Zoom in и View-Zoom out. Средства для рисования в редакторе пиктограмм представлены двумя окнами: окном инструментов и окном цветовой палитры. Мы не будем описывать все возможности редактора пиктограмм, тем более, что их можно изучить, даже не открывая руководство по Resource Workshop. Обратим ваше внимание только на следующий момент. В палитре существует два цвета, которые выделены среди остальных. Первый называется Transparent (прозрачный). Все, что рисуется этим “цветом”, при отображении пиктограммы оказывается прозрачным. Второй цвет называется Inverted (инвертированный). При отображении пиктограммы этот “цвет” инвертирует фоновое изображение. Использование цветов Transparent и Inverted позволяет на одном изображении неявно редактировать сразу две маски (AND и XOR), из которых состоит пиктограмма. Цвета, используемые для пометки прозрачных и инвертирующих фон элементов пиктограмм, могут совпадать с другими цветами па- литры. Однако логически это разные цвета. Поэтому, если вы желаете рисовать изображение цветом, совпадающим с тем, который исполь- зуется для обозначения прозрачности, цвет Transparent лучше изме- нить. Это можно сделать после двухкратного щелчка мыши на нем в окне Colors. При этом автоматически изменяется и цвет Inverted. Мы уже отмечали, что в Windows одним элементом ресурса может быть группа пиктограмм разного формата. В этом случае идентифи- катор присваивается всей группе. Для того чтобы добавить в группу пиктограмму другого формата, нужно перейти в окно группы (небольшое окно в левой верхней части главного окна) и выбрать в меню Images команду New image.... 5.6.3. ПОДКЛЮЧЕНИЕ ПИКТОГРАММ Допустим, мы разработали некоторую пиктограмму и сохранили ее в файле ICON.RES с идентификатором idi_Icon (100). Рассмотрим; как подключить ее к главному окну программы так, чтобы она отобража- лась при его минимизации. Пиктограмма в Windows принадлежит не окну, а оконному классу. В записи оконного класса TWndClass существует поле Ысоп - деск- риптор пиктограммы, отображаемой при минимизации окон этого класса. По умолчанию в поле Ысоп помещается дескриптор стандарт- ной пиктограммы Windows, изображенной на рис. 5.7.
Глава 5. Ресурсы Windows 191 Рис. 5.7. Стандартная пиктограмма для окон приложений Эту пиктограмму имели все наши предыдущие OWL-приложения. Чтобы в оконном классе определить другую пиктограмму, нужно в объекте главного окна перекрыть виртуальный метод GetWindowClass: TMainWindow = object(TWindow) function GetClassName: PChar; virtual; procedure GetWindowClass(var AWndClass: TWndClass); virtual; end; Новый оконный класс всегда регистрируется с новым именем, по- этому, кроме метода GetWindowClass, в TMainWindow перекрыт еще и метод GetClassName. В нашем примере GetClassName возвращает строку 'IconDemo'. Напомним, что метод GetWindowClass вызывается автоматически для формирования записи оконного класса перед реги- страцией. В объекте TMainWindow этот метод сначала заполняет структуру AWndClass значениями по умолчанию, вызывая одноимен- ный метод предка, а затем загружает из ресурса пиктограмму и при- сваивает ее дескриптор полю hlcon в записи AWndClass: procedure TMainWindow.GetWindowClass(var AWndClass: TWndClass); begin inherited GetWindowClass(AWndClass); AWndClass.hlcon := Loadlcon(Hlnstance, PChar(idiJcon)); end; Для загрузки пиктограммы из ресурса используется функция Loadicon, которая объявлена в WINPROCS следующим образом: Loadlcon(lnstance: THandle; IconName: PChar): Hlcon; где Instance - дескриптор копии приложения, содержащего в своем ресурсе пиктограмму; IconName - идентификатор пиктограммы в ресурсе. Если параметр Instance равен нулю, то загружается одна из стандартных пиктограмм Windows. При этом в параметре IconName передается одна из констант табл. 5.5. Функция Loadicon возвращает дескриптор загруженной пиктограммы. В приведенном выше примере он сохраняется в соответствующем элементе данных оконного класса. Следует заметить, что функция Loadicon не загружает ресурс сразу в оперативную память, а всего лишь ставит в соответствие дескрипто- ру пиктограммы место ее расположения в выполняемом файле. В опе- ративную память пиктограмма загружается только при непосредст- венном отображении на экран.
192 Программирование в среде Borland Pascal для Windows Табл. 5.5. Идентификаторы стандартных пиктограмм Константа Описание idi_Application Пиктограмма по умолчанию idi.Hand Пиктограмма, на которой изображен знак “STOP” idi-Question Пиктограмма, на которой изображен символ “?” idi_Exclamation Пиктограмма, на которой изображен символ “!” idi_Asterisk Пиктограмма, на которой изображена буква “i” Ниже приводится текст простейшей программы ICON, демонстри- рующей установку пиктограммы в оконном классе: ICON.INC const idijcon = 100; ICON.PAS program Icon; {$R ICON.RES} uses WinTypes, WinProcs, OWindows; {$1 ICON.INC} type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) function GetClassName: PChar; virtual; procedure GetWindowClass(var AWndClass: TWndClass); virtual; end; PIconApp = ATlconApp; TIconApp = object(TApplication) procedure InitMainWindow; virtual; end; { Возвращает имя нового оконного класса } function TMainWindow.GetClassName: PChar; begin GetClassName := ’IconDemo’; end; { Устанавливает в оконном классе новую пиктограмму} procedure TMainWindow.GetWindowClass(var AWndClass: TWndClass); begin inherited GetWindowClass(AWndClass); AWndClass.hlcon := Loadlcon(HInstance, PChar(idiJcon)); end; { Создает объект главного окна программы } procedure TIconApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Icon Demo’)); end;
Глава 5. Ресурсы Windows 193 var IconApp: TIconApp; begin lconApp.lnit('lcon’); IconApp.Run; IconApp. Done; end. 5.6.4. ОТОБРАЖЕНИЕ ПИКТОГРАММ Для вывода пиктограммы используется следующая функция: DrawIcon(DC: HDC; X, Y: Integer; Icon: Hlcon): Bool; где DC - контекст устройства, на которое осуществляется вывод; X, Y - координаты левого верхнего угла пиктограммы; Icon - деск- риптор пиктограммы (обычно результат вызова функции Loadicon). При успешном завершении Drawicon возвращает True. Функция Drawicon отображает пиктограмму оригинального раз- мера. Если при выводе требуется масштабирование, то необходимо использовать дополнительные средства работы с растровыми изо- бражениями, которые мы рассмотрим в 5.8. Следующая программа демонстрирует использование функции Drawicon. Она отображает в окне пять стандартных пиктограмм Windows и свою собственную пиктограмму (рис. 5.8): ICON.INC const idijcon = 100; Рис. 5.8. Окно программы SHOWICON 7 Зак. 1049
194 Программирование в среде Borland Pascal для Windows SHOWICON.PAS program Showicon; {$R ICON.RES} uses WinTypes, WinProcs, OWindows; {$1 ICON.INC} type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) function GetClassName: PChar; virtual; procedure GetWindowClass(var AWndClass: TWndClass); virtual; procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; PShowIconApp = ATShowlconApp; TShowIconApp = object(TApplication) procedure InitMainWindow; virtual; end; { Возвращает имя нового оконного класса } function TMainWindow.GetClassName: PChar; begin GetClassName := 'Showicon'; end; { Устанавливает в оконном классе новую пиктограмму} procedure TMainWindow.GetWindowClass(var AWndClass: TWndClass); begin inherited GetWindowClass(AWndClass); AWndClass.hlcon := Loadlcon(HInstance, PChar(idiJcon)); end; { Рисует в окне 6 пиктограмм: 5 стандартных и 1 пользователя } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); begin Drawlcon(PaintDC, 40, 40, Loadlcon(0, idi_Application)); Drawlcon(PaintDC, 140,40, Loadlcon(0, idi_Hand)); Drawlcon(PaintDC, 240,40, Loadlcon(0, idi_Question)); Drawlcon(PaintDC, 40,140, Loadlcon(0, idi_Exclamation)); Drawlcon(PaintDC, 140,140, Loadlcon(0, idi_Asterisk)); Drawlcon(PaintDC, 240,140, Loadlcon(Hlnstance, PChar(idiJcon))); end; { Создает объект главного окна программы } procedure TShowIconApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, Various Icons')); end; var ShowIconApp: TShowIconApp; begin ShowlconApp.lnit('Showlcon'); ShowIconApp.Run; ShowIconApp. Done; end.
Глава 5. Ресурсы Windows 195 5.6.5. ДИНАМИЧНЫЕ ПИКТОГРАММЫ Windows-программа может сама отображать пиктограмму минимизи- рованного окна. Для этого дескриптор пиктограммы в оконном клас- се должен быть установлен в нуль. Если окно создается на основе класса, в котором пиктограмма отсутствует, то вся ответственность за пиктограмму минимизированного окна возлагается на приложение. Рисование в этой области осуществляется точно так же, как и рисова- ние в обычной рабочей области окна, т.е. по сообщению wmJPaint. Отличие состоит лишь в том, что после минимизации окна его рабо- чая область устанавливается равной области пиктограммы. При ри- совании может использоваться функция Islconic, которая проверяет, является ли окно минимизированным. Если вывод производить по сообщениям от таймера, то можно по- лучить динамичную пиктограмму, которая изменяется со временем. Примером служит программа DYNICON, которая при минимизации отображает текущее время: program Dynlcon; uses WinTypes, WinProcs {$ifdefVer80}, Messages {$endif}, WinDos, Strings, OWindows; const Timerld = 1; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) function GetClassName: PChar; virtual; procedure GetWindowClass(var AWndClass: TWndClass); virtual; procedure SetupWindow; virtual; procedure WMDestroy(var Msg: TMessage); virtual wm_First + wmDestroy; 4 procedure WMTimdr(var Msg: TMessage); virtual wm_First + wm_Timer; procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; PDynlconApp = ATDynlconApp; TDynlconApp = object(TApplication) procedure InitMainWindow; virtual; end; { Возвращает имя нового оконного класса } function TMainWindow.GetClassName: PChar; begin GetClassName := 'Dynamicicon'; end; { Убирает в оконном классе пиктограмму } procedure TMainWindow.GetWindowClass(var AWndClass: TWndClass); begin inherited GetWindowClass(AWndClass);
196 Программирование в среде Borland Pascal для Windows AWndClass.hlcon := 0; end; {После создания окна устанавливает таймер на интервал 1с} procedure TMainWindow.SetupWindow; begin inherited SetupWindow; SetTimer(HWindow, Timerld, 1000, nil); end; { Удаляет таймер при уничтожении окна } procedure TMainWindow. WMDestroy(var Msg: TMessage); begin KillTimer(HWindow, Timerld); inherited WMDestroy(Msg); end; { Вызывает обновление пиктограммы окна по сообщениям от таймера } procedure TMainWindow.WMTimer(var Msg: TMessage); begin if Islconic(HWindow) then lnvalidateRect(HWindow, nil, False); end; { Выводит текущее время в качестве пиктограммы окна } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var Time: record Hour: Word; Minute: Word; Second: Word; SedOO: Word; end; R: TRect; OldBrush: HBrush; Str: array [0..15] of Char; begin if Islconic(HWindow) then begin with Time do GetTime(Hour, Minute, Second, Sec100); OldBrush := SelectObject(PaintDC, GetStockObject(Black_Brush)); GetClientRect(HWindow, R); Rectangle(PaintDC, R.Left, R.Top, R.Right, R.Bottom); WVSPrintF(Str, *%02i:%02i’, Time); lnflateRect(R, -1, -1); SetTextColor(PaintDC, RGB(0,128, 0)); SetBkMode(PaintDC, Transparent); DrawText(PaintDC, Str, -1, R, dt_SingleLine or dt_Top or dt_Right); WVSPrintF(Str, ’%02i', Time.Second); R.Left := R.Left + (R.Right - R.Left) div 2; SetTextColor(PaintDC, RGB(128, 0, 0)); DrawText(PaintDC, Str, -1, R, dt_SingleLine or dt_Bottom or dt_Right); SelectObject(PaintDC, OldBrush); end; end; { Создает объект главного окна программы } procedure TDynlconApp.lnitMainWindow;
Гпава 5. Ресурсы Windows 197 begin MainWindow := New(PMainWindow, lnit(nil, ’Dynamic Icon')); end; var DynlconApp: TDynlconApp; begin DynlconApp.lnit(’Dynlcon’); DynlconApp. Run; DynlconApp.Done; end. Чтобы осуществлять контроль над пиктограммой минимизиро- ванного окна, в объекте TMainWindow перекрыт метод GetWindowClass. Он устанавливает дескриптор пиктограммы оконно- го класса в нуль. Метод GetClassName возвращает имя нового окон- ного класса - ’Dynamicicon’. Обновление содержимого окна произво- дится по сообщениям от таймера, который устанавливается в методе SetupWindow на интервал времени 1 с. Удаление таймера происходит в методе WMDestroy, вызываемом автоматически при уничтожении окна. Сообщения от таймера обрабатываются в методе WMTimer, который проверяет, минимизировано ли окно, и помечает его для перерисовки. Перерисовку окна осуществляет метод Paint, который выводит время в цифровой форме с точностью до секунды. Часы и минуты отображаются зеленым цветом, а секунды - красным. 5.7. КУРСОРЫ 5.7.1. ЧТО ТАКОЕ КУРСОР Курсор (cursor) - это небольшой битовый образ, который использует- ся в качестве указателя мыши на экране. Курсоры имеют много обще- го с пиктограммами. Фактически они и являются черно-белыми пик- тограммами размерами 32x32 или 32x16. В ресурсе может храниться несколько курсоров различного формата с одним идентификатором. В связи с тем, что курсоры размеров 32x16 нужны только для уста- ревших CGA-адаптеров, с которыми Windows уже не используется, программы обычно содержат только образы курсоров размерами 32x32. ' * Как и пиктограмма, курсор состоит из двух масок (AND и XOR), которые задают его форму. Однако в отличие от пиктограммы курсор используется как указатель, поэтому в нем есть еще так называемая “горячая точка" (hot spot). Именно она определяет положение курсора на экране. Все остальные части курсора создают только внешний эф- фект. За исключением “горячей точки”, внутренний формат курсора не отличается от формата пиктограммы.
198 Программирование в среде Borland Pascal для Windows Курсоры могут храниться в отдельных файлах с расширением CUR или в RES-файле совместно с другими ресурсами. На этапе ком- поновки программы они встраиваются в исполняемый ЕХЕ-модуль. Мы не будем рассматривать разработку курсоров с помощью Resource Workshop, так как она почти не отличается от разработки пиктограмм. Различие состоит лишь в том, что курсоры являются черно-белыми и имеют размеры 32x32. Редактор Resource Workshop не поддерживает для курсоров множественных форматов. 5.7.2. УСТАНОВКА КУРСОРА На экране всегда существует единственный указатель мыши, с помо- щью которого осуществляется управление всеми окнами. Форма ука- зателя, т.е. вид курсора, обычно соответствует назначению окна. На- пример, в окне текстового редактора курсор имеет вид 1-луча, а при попадании на рамку изменения размера он принимает форму разно- направленных стрелок. Windows автоматически видоизменяет курсор при перемещении указателя мыши из одного окна в другое. С этой целью для каждого оконного класса ведется свой курсор. Его дескриптор записывается в поле hCursor структуры TWndClass при регистрации оконного класса. По умолчанию объект TWindow устанавливает в поле hCursor деск- риптор стандартного курсора Windows, имеющего форму стрелки. Чтобы установить в окне другой курсор, нужно перекрыть в оконном объекте метод GetWindowClass: procedure TMainWindow.GetWindowClass(var AWndClass: TWndClass); begin inherited GetWindowClass(AWndClass); AWndClass.hCursor := LoadCursor(Hlnstance, PChar(idc_Cursor)); end; Метод GetWindowClass написан в предположении, что в ресурсе программы существует курсор с идентификатором idc_Cursor. Для его загрузки вызывается функция LoadCursor(Instance: THandle; CursorName: PChar): HCursor; где Instance - дескриптор копии приложения, загружающего курсор; CursorName - идентификатор курсора в файле ресурса. Если иденти- фикатором служит число, оно должно быть преобразовано к типу PChar (или MakelntResource). Функция возвращает дескриптор кур- сора в памяти. LoadCursor может использоваться для загрузки стандартных кур- соров Windows. Для этого в параметре Instance передается нуль, а в параметре CursorName - константа из табл. 5.6.
Гпава 5. Ресурсы Windows 199 Табл. 5.6. Идентификаторы стандартных курсоров Константа Описание idc_Arrow idc_Beam idc.Wait idc_Cross idc_UpArrow idc_Size Курсор в виде стрелки (по умолчанию) 1-луч в редакторе текста Песочные часы как признак ожидания Перекрестие Стрелка, направленная вверх Четыре разнонаправленные стрелки. Курсор предназначен для изменения размеров окна idc.Icon idc_SizeNWSE Пустая пиктограмма (широкая прямоугольная рамка) Две разнонаправленные стрелки, указывающие на северо- запад и юго-восток. Курсор предназначен для изменения размеров окна idc_SizeNESW Две разнонаправленные стрелки, указывающие на северо- восток и юго-запад. Курсор предназначен для изменения размеров окна idc.SizeWE Две разнонаправленные стрелки, указывающие на запад и восток. Курсор предназначен для изменения размеров окна idc_SizeNS Две разнонаправленные стрелки, указывающие на север и юг. Курсор предназначен для изменения размеров окна Приведем пример подключения курсора, разработанного в Resource Workshop и сохраненного в файле CURSOR.RES: CURSOR.INC const idc_Cursor = 100; CURSOR.PAS program Cursor; {$R CURSOR.RES} uses WinTypes, WinProcs, OWindows; {$1 CURSOR.INC} type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) function GetClassName: PChar; virtual; 0 procedure GetWindowClass(var AWndClass: TWndClass); virtual; end; PCursorApp = ATCursorApp; TCursorApp = object(TApplication) procedure InitMainWindow; virtual; end; { Возвращает имя нового оконного класса } function TMainWindow.GetClassName: PChar;
200 Программирование в среде Borland Pascal для Windows begin GetClassName := ’CursorDemo’; end; { Устанавливает в оконном классе новый курсор, загружая его из ресурса } procedure TMainWindow.GetWindowClass(var AWndClass: TWndClass); begin inherited GetWindowCiass(AWndClass); AWndClass.hCursor := LoadCursor(H Instance, PChar(idc_Cursor)); end; { Создает объект главного окна программы } procedure TCursorApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Cursor Demo’)); end; var CursorApp: TCursorApp; begin CursorApp.lnit(’Cursor’); CursorApp. Run; CursorApp.Done; end. Подключение курсора в программе CURSOR очень мало отлича- ется от подключения пиктограммы в примере ICON. Различие лишь в том, что метод TMainWindow.GetWindowClass загружает и устанав- ливает в оконном классе курсор, а не пиктограмму. 5.7.3. ВРЕМЕННАЯ УСТАНОВКА КУРСОРА В некоторых программах установка формы курсора может происхо- дить не раз и навсегда, а только на время выбранного режима работы. Например, при рисовании картинки в Resource Workshop форма кур- сора всегда соответствует текущему графическому инструменту и из- меняется при выборе нового. Временную установку курсора можно реализовать в программе двумя принципиально разными способами. Первый способ состоит в замене курсора оконного класса. В этом случае курсор изменяется при попадании мыши в любое окно этого класса. Второй, более низко- уровневый, способ заключается в замене курсора, назначенного в текущий момент указателю мыши. Рассмотрим сначала первый спо- соб. Для смены курсора в оконном классе применяется функция SetClassWord(Wnd: HWnd; Index: Integer; NewWord: Word): Word; где Wnd - окно, которое изменяет свой оконный класс (нужно учиты- вать, что один класс обслуживает множество окон и его изменение затрагивает все эти окна); Index - смещение в области данных оконно-
Гпава 5. Ресурсы Windows 201 го класса; New Word - двухбайтное значение, записываемое по смеще- нию Index. Функция возвращает прежнее значение установленного атрибута. Функция SetClassWord служит для изменения любых двухбайтных атрибутов оконного класса, а не только курсора. Изменяемый атри- бут определяется значением параметра Index: • gcw_HBRBackGround (-10) - дескриптор кисти; • gcw_HCursor (-12) - дескриптор курсора; • gcw_HIcon (-14) - дескриптор пиктограммы; • gcw_HModule (-16) - дескриптор экземпляра приложения; • gcw_CBWndExtra (-18) - дополнительное число байт, выделенное вслед за данными окна; • gcw.CBClsExtra (-20) - дополнительное число байт, выделенное вслед за данными класса; • gcw_Style (-26) - стиль оконного класса. Если при регистрации оконного класса для него резервировалась дополнительная память (атрибут cbClsExtra записи TWndClass боль- ше нуля), то, чтобы ее изменить, нужно указать в параметре Index соответствующее положительное смещение (отсчет ведется от нуля). При установке курсора в параметре Index передается gcw_HCursor, а в параметре NewWord - его дескриптор. Если по ходу работы программы нужно выполйять смену курсо- ров, загружая их из ресурса, то в оконном объекте удобно определить метод, который это делает: procedure TMainWindow.SetClassCursor(lnst: THandle; Cursorld: PChar); begin DestroyCursor(SetClassWord(HWindow, gcw_HCursor, LoadCursor(lnst, Cursorld))); end; Процедура SetClassCursor имеет два параметра: дескриптор при- ложения (Inst) и идентификатор курсора в ресурсе (Cursorld). Она загружает курсор (LoadCursor), назначает его оконному классу (SetClassWord), а затем удаляет предыдущий курсор (DestroyCursor). Ниже приводится программа SHOWCURS, которая использует метод SetClassCursor для демонстрации различных курсоров Windows, в том числе и пользовательского курсора, разработанного в Resource Workshop. При запуске программы в окне отображается меню Cursor, в котором по названию можно выбрать курсор, устанавливаемый в оконном классе: SHOWCURS.INC const idm_Menu = 100; idc_Cursor = 100;
202 Программирование в среде Borland Pascal для Windows cm_CursorArrow = 101; cm_CursorlBeam = 102; cm_CursorWait = 103; cm_CursorCross = 104; cm_CursorUpArrow = 105; cm_CursorSize = 106; cm_Cursorlcon = 107; cm_CursorSizeNWSE = 108; cm_CursorSizeNESW = 109; cm_CursorSizeWE = 110; cm__CursorSizeNS = 111; cm_CursorUser = 112; SHOWCURS.PAS program ShowCurs; {$R SHOWCURS.RES} uses WinTypes, WinProcs, OWindows; {$1 SHOWCURS. INC} type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) constructor lnit(AParent: PWindowsObject; ATitle: PChar); procedure SetClassCursor(lnst: THandle; Cursorld: PChar); procedure CMCursorArrow(var Msg: TMessage); virtual cmJFirst + cm_CursorArrow; procedure CMCursorlBeam(var Msg: TMessage); virtual cmJFirst + cm_CursorlBeam; procedure CMCursorWait(var Msg: TMessage); virtual cmJFirst + cm_CursorWait; procedure CMCursorCross(var Msg: TMessage); virtual cm_.First + cm_CursorCross; procedure CMCursorUpArrow(var Msg: TMessage); virtual cmJFirst + cm_CursorUpArrow; procedure CMCursorSize(var Msg: TMessage); virtual cmJFirst + cm_CursorSize; procedure CMCursorlcon(var Msg: TMessage); virtual cm_First + cm_Cursorlcon; procedure CMCursorSizeNWSE(var Msg: TMessage); virtual cmJFirst + cm_CursorSizeNWSE; procedure CMCursorSizeNESW(var Msg: TMessage); virtual cmJFirst + cm_CursorSizeNESW; procedure CMCursorSizeWE(var Msg: TMessage); virtual cmJFirst + cm_CursorSizeWE; procedure CMCursorSizeNS(var Msg: TMessage); virtual cm_First + cm_CursorSizeNS; procedure CMCursorUser(var Msg: TMessage); virtual cm_First + cm_CursorUser; end; PShowCursorApp = ATShowCursorApp;
Глава 5. Ресурсы Windows 203 TShowCursorApp = object(TApplication) procedure InitMainWindow; virtual; end; { Конструирует оконный объект и загружает из ресурса меню } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); Attr.Menu := LoadMenu(Hlnstance, PChar(idm_Menu)); end; { Заменяет e оконном классе курсор, загружая другой из ресурса } procedure TMainWindow.SetClassCursor(lnst: THandle; Cursorld: PChar); begin DestroyCursor(SetClassWord(HWindow, gcw_HCursor, LoadCursor(lnst, Cursorld))); end; { По команде меню Cursor-Arrow устанавливает курсор idc_Arrow } procedure TMainWindow.CMCursorArrow(var Msg: TMessage); begin SetClassCursor(0, idc_Arrow); end; { По команде меню Cursor-IBeam устанавливает курсор idcJBeam } procedure TMainWindow.CMCursorlBeam(var Msg: TMessage); begin SetClassCursor(0, idcJBeam); end; { По команде меню Cursor-Wait устанавливает курсор idc_Wait} procedure TMainWindow.CMCursorWait(var Msg: TMessage); begin SetClassCursor(0, idc_Wait); end; { По команде меню Cursor-Cross устанавливает курсор idc_Cross } procedure TMainWindow.CMCursorCross(var Msg: TMessage); begin SetClassCursor(0, idc_Cross); end; { По команде меню Cursor-UpArrow устанавливает курсор idc_UpArrow } procedure TMainWindow.CMCursorUpArrow(var Msg: TMessage); begin SetClassCursor(0, idcJJpArrow); end; { По команде меню Cursor-Size устанавливает курсор idc_Size } procedure TMainWindow.CMCursorSize(var Msg: TMessage); ъ begin SetClassCursor(0, idc_Size); end; { По команде меню Cursor-Icon устанавливает курсор idc_lcon } procedure TMainWindow.CMCursorlcon(var Msg: TMessage); begin SetClassCursor(0, idcjcon); end;
204 Программирование в среде Borland Pascal для Windows { По команде меню Cursor-Size \ устанавливает курсор idc_SizeNWSE} procedure TMainWindow.CMCursorSizeNWSE(var Msg: TMessage); begin SetClassCursor(0, idc_SizeNWSE); end; { По команде меню Cursor-Size / устанавливает курсор idc_SizeNESW} procedure TMainWindow.CMCursorSizeNESW(var Msg: TMessage); begin SetClassCursor(0, idc_SizeNESW); end; { По команде меню Cursor-Size - устанавливает курсор idc_SizeWE} procedure TMainWindow.CMCursorSizeWE(var Msg: TMessage); begin SetClassCursor(0, idc_SizeWE); end; { По команде меню Cursor-Size | устанавливает курсор idc_SizeNS } procedure TMainWindow.CMCursorSizeNS(var Msg: TMessage); begin SetClassCursor(0, idc_SizeNS); end; { По команде меню Cursor-User устанавливает курсор пользователя } procedure TMainWindow.CMCursorUser(var Msg: TMessage); begin SetClassCursor(Hlnstance, PChar(idc_Cursor)); end; { Создает объект главного окна программы } procedure TShowCursorApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, Various Cursors')); end; var ShowCursorApp: TShowCursorApp; begin ShowCursorApp.lnit('ShowCurs'); ShowCursorApp. Run; ShowCursorApp.Done; end. Второй способ подмены курсора предполагает непосредственную работу с указателем мыши. Он основан на использовании функции SetCursor(Cursor: HCursor): HCursor; где Cursor - дескриптор устанавливаемого курсора. Если этот пара- метр равен нулю, то курсор с экрана пропадает. Функция SetCursor назначает указателю мыши новый курсор и возвращает дескриптор предыдущего. Подмена происходит только в том случае, когда новый курсор отличается от прежнего, что сущест- венно ускоряет работу функции. Следует учитывать, что после вызова SetCursor установленный курсор “слетает” при любом перемещении
Гпава 5. Ресурсы Windows 205 мыши, так как Windows автоматически заменяет его на курсор окон- ного класса (вызывая со своей стороны SetCursor). Однако если на время подмены курсора окно захватывает мышь с помощью функции SetCapture, то этого не происходит. Установка с помощью функции SetCursor курсора в виде песочных часов часто используется в длительных операциях для информирова- ния пользователя о том, что ему следует немного подождать. Напри- мер, фрагмент длительного по времени участка программы может выглядеть так: var Curs: HCursor; begin Curs := SetCursor(LoadCursor(0, idc_Wait)); {Длительная операция } DestroyCursor(SetCursor(Curs)); end; Сначала загружается и устанавливается курсор в форме песочных часов, после этого выполняется длительная операция, а затем прежний курсор восстанавливается. Если процедуры установки и восстановления курсора выполняют- ся по различным сообщениям Windows, то перед установкой курсора нужно захватить мышь с помощью функции SetCapture, а после вос- становления курсора освободить ее вызовом ReleaseCapture. Так ра- ботает программа SETCURS, которая захватывает мышь и устанав- ливает курсор по нажатии ее левой кнопки и восстанавливает курсор, освобождая мышь, при отпускании кнопки: program SetCurs; {$R CURSOR.RES} uses WinTypes, WinProcs {$ifdefVer80}, Messages {$endif}, ©Windows; const idc_Cursor = 100; type PMainWindow = ATMainWindow; TMainWindow = objectfTWindow) Cursor: HCursor; { временный курсор мыши } procedure WMLButtonDown(var Msg: TMessage); virtual wm_First + wm_LButtonDown; ° procedure WMLButtonUp(var Msg: TMessage); virtual wm_First + wm_LButtonllp; end; PSetCursorApp = ATSetCursorApp; TSetCursorApp = object(TApplication) procedure InitMainWindow; virtual; end;
206 Программирование в среде Borland Pascal для Windows { При нажатии кнопки захватывает мышь и устанавливает другой курсор } procedure TMainWindow.WMLButtonDown(var Msg: TMessage); begin SetCapture(HWindow); Cursor := SetCursor(LoadCursor(Hlnstance, PChar(idc_Cursor))); end; {При отпускании кнопки восстанавливает курсор и освобождает мышь} procedure TMainWindow.WMLButtonUp(var Msg: TMessage); begin SetCursor(Cursor); ReleaseCapture; end; { Создает объект главного окна программы } procedure TSetCursorApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Set Cursor’)); end; var SetCursorApp: TSetCursorApp; begin SetCursorApp.lnit(’SetCurs’); SetCursorApp.Run; SetCursorApp. Done; end. 5.8. РАСТРОВЫЕ ИЗОБРАЖЕНИЯ 5.8.1. ОСНОВНЫЕ ПОНЯТИЯ Растровое изображение, или битовый образ (bitmap), - это представ- ление графической информации, которое ориентировано на растро- вые и матричные устройства вывода (дисплей, принтер). Растровое изображение состоит из цветовых точек (пикселов), организованных в матрицу. Оно может быть цветным или монохромным (черно-белым). Формат растрового изображения достаточно сложный и включает информацию о точках, размерах, используемых цветах и пр. Windows позволяет использовать растровые изображения двух ви- дов: аппаратно-зависимые (от англ. Device-Dependent Bitmap - DDB) и аппаратно-независимые (от англ. Device-Independent Bitmap - DIB). Аппаратно-зависимые растровые изображения рассчитаны только на определенный тип графического адаптера или принтера. Их точки находятся в прямом соответствии с пикселами экрана или другой по- верхности отображения. Информация о цветах представляется бито- выми планами, организованными в соответствии со спецификой уст- ройства вывода. Как следствие, растровые изображения, предназна- ченные для адаптера EGA, отличаются по формату от изображений, созданных в расчете на CGA- или VGA-адаптеры. Аппаратно-
Гпава 5. Ресурсы Windows 207 зависимый формат хранения обеспечивает наибольшую скорость вы- вода изображений, поэтому очень подходит для работы с растровыми изображениями в оперативной памяти. Формат хранения аппаратно-независимых растровых изображений не зависит от типа графического адаптера или другого устройства вывода. В них информация о цвете и собственно изображении хранит- ся раздельно. Цвета собраны в таблицу. Точки изображения кодиру- ют номера цветов в таблице. Под каждую точку может отводиться 1, 4, 8 или 24 бита. Достоинством такого способа хранения является высокая гибкость и простота в работе. Например, при изменении цве- та в таблице цветов изменяются все точки изображения с таким цве- том. Еще одно преимущество DIB-изображений состоит в возможно- сти использования алгоритма сжатия данных RLE (от англ. Run- Length Encoding). Сжатые DIB-изображения занимают меньше места на диске, хотя и требуют больше времени при загрузке. “Разжатие” изображений происходит автоматически, незаметно для программи- ста. В силу своей универсальности аппаратно-независимый формат используется для хранения растровых изображений на диске. 5.8.2. РАЗРАБОТКА РАСТРОВЫХ ИЗОБРАЖЕНИЙ Растровые изображения могут храниться либо вместе с другими ре- сурсами, либо в отдельных файлах с расширением BMP. Преимущест- во использования BMP-файлов состоит в возможности простого пе- реноса растровых изображений между программами. Для разработки растровых изображений вы можете использовать Resource Workshop, Paint Brush или любой другой графический ре- дактор, позволяющий сохранять изображение в формате BMP. Гото- вое изображение легко добавить в файл ресурса, если в меню Resource Workshop выбрать команду File-Add to project... и ввести имя соответ- ствующего ВМР-файла. Resource Workshop имеет достаточно мощный редактор растровых изображений, с помощью которого можно создавать шедевры ком- пьютерной графики. Мы рекомендуем его как профессионалам, так и новичкам. Если вы только начинаете разработку картинки, откройте или создайте заново RC-проект и выберите в меню Resource команду New.... В появившемся диалоговом окне укажите тип ресурса BITMAP. Resource Workshop предложит определить форму хранения картинки: в виде исходного текста в RC-файле или в двоичном фор- мате в виде отдельного ВМР-файла. Предпочтение следует отдать второму способу, позволяющему использовать картинку в других программах. После ввода имени ВМР-файла на экране появится диа- логовое окно, в котором задаются размеры изображения и количество одновременно используемых цветов. Определив эти параметры, мож- но приступать к рисованию.
208 Программирование в среде Borland Pascal для Windows Редактор растровых изображений Resource Workshop имеет ин- туитивно понятный интерфейс. Вы быстро найдете там “холст”, “кисть” и “краски”, поэтому не будем тратить время на скучное опи- сание окон и команд меню, а перейдем к загрузке и выводу картинок в программе. Но прежде сделаем несколько замечаний: Размер и количество одновременно используемых цветов изобра- жения можно изменять в процессе редактирования. Для этого в меню Bitmap существует команда Size and attributes.... В диалоговом окне, которое создается по этой команде, указывается также, нужно ли сжимать изображение по алгоритму RLE и тип сжатия: RLE 4 или RLE 8 (цифры 4 и 8 определяют параметры алгоритма). Сжатие следу- ет использовать с большой осторожностью, так как не все драйверы дисплея поддерживают разжатие растровых изображений. Если в дан- ный момент установлен драйвер, который не поддерживает разжатие, то вы просто не сможете создать сжатую картинку в Resource Workshop. 5.8.3. ЗАГРУЗКА И ВЫВОД РАСТРОВЫХ ИЗОБРАЖЕНИЙ Загрузка растрового изображения из ресурса осуществляется с помо- щью функции LoadBitmap(Instance: THandle; BitmapName: PChar): HBitmap; где Instance - дескриптор экземпляра приложения; BitmapName - идентификатор ресурса, в качестве которого обычно используется целочисленная константа, преобразованная к типу PChar. Функция возвращает дескриптор, который сохраняется в переменной типа HBitmap и используется во всех операциях с данным изображением. Например: Bitmap := LoadBitmap(Hlnstance, PChar(idb Bitmap)); Функция LoadBitmap может использоваться для загрузки стан- дартных растровых изображений Windows: образов системного меню, кнопок максимизации и минимизации и т.д. Для этого в параметре Instance передается нуль, а в параметре BitmapName - соответствую- щая константа (имена этих констант начинаются с obm_). Стандарт- ные растровые изображения редко нужны в программе, поэтому мы их не рассматриваем. После использования растровое изображение обязательно должно быть удалено: DeleteObject(Bitmap); Если этого не сделать, то после завершения программы растровое изображение будет оставаться в памяти вплоть до выхода из Windows.
Гпава 5. Ресурсы Windows 209 Рассмотрим теперь, как осуществляется вывод растровых изобра- жений. Это сложная операция, поэтому мы рекомендуем вам внима- тельно с ней разобраться. Прежде всего напомним, что вывод графической информации в Windows осуществляется с помощью контекста устройства - DC. Вы- вод растрового изображения предполагает использование двух кон- текстов - физического и виртуального. Контекст устройства не обяза- тельно должен быть связан с физическим устройством вывода. Windows позволяет создать контекст виртуального устройства, по- верхностью отображения которого является растровое изображение в оперативной памяти. Чтобы вывести растровое изображение на физи- ческое устройство, сначала необходимо создать контекст виртуально- го устройства и назначить ему в качестве поверхности рисования рас- тровое изображение. После этого нужно перенести образ из виртуаль- ного контекста в контекст устройства, ассоциированный с окном на экране дисплея. В ходе такого переноса возможно масштабирование изображения в сторону сжатия или растяжения. Например, следующая процедура выводит в окно заданное растро- вое изображение: procedure ShowBitmap(HWindow: HWnd; Bitmap: HBitmap); var DC, MemDC: HDC; OldBitmap: HBitmap; begin { Получаем физический контекст устройства для окна } DC :='GetDC(HWindow); { Создаем контекст виртуального устройства - MemDC } MemDC := CreateCompatibleDC(DC); { Устанавливаем в контексте MemDC растровое изображение } OldBitmap := SelectObject(MemDC, Bitmap); { Копируем изображение из контекста MemDC в контекст DC } BitBlt(DC, 10, 10, 64, 64, MemDC, 0, 0, SrcCopy); { Восстанавливаем в контексте MemDC прежнее растровое изображение } SelectObject(MemDC, OldBitmap); { Удаляем виртуальный контекст устройства } DeleteDC(MemDC); { Освобождаем физический контекст устройства } ReleaseDC(HWindow, DC); end; о Сначала с помощью функции GetDC запрашивается контекст дис- плея (переменная DC) для окна HWindow. После этого вызовом CreateCompatibleDC создается контекст виртуального устройства, совместимый с оконным контекстом дисплея. Его дескриптор сохра- няется в локальной переменной MemDC. С помощью функции SelectObject в контексте MemDC устанавливается новая поверхность
210 Программирование в среде Borland Pascal для Windows рисования - растровое изображение Bitmap. Предыдущая поверхность сохраняется в локальной переменной OldBitmap. Запомните: только в виртуальном контексте можно заменить поверхность рисования. С помощью функции BitBlt поверхность рисования контекста MemDC (или ее часть) переносится на поверхность контекста DC, т.е. растро- вое изображение копируется из памяти на экран. После вывода рас- трового изображения во временном контексте MemDC восстанавли- вается прежняя поверхность OldBitmap и контекст уничтожается вы- зовом функции DeleteDC. В заключение освобождается и контекст дисплея DC с помощью функции ReleaseDC. Основную логику работы рассмотренного фрагмента программы реализует функция BitBlt(DestDC: HDC; X, Y, Width, Height: Integer; SrcDC: HDC; XSrc, YSrc: Integer; Rop: Longint): Bool; где DestDC - контекст устройства приемника; X, Y - координаты ле- вого верхнего угла результирующего растрового изображения в кон- тексте-приемнике DestDC; Width, Height - ширина и высота растрово- го изображения; SrcDC - контекст устройства источника; XSrc, YSrc - координаты левого верхнего угла изображения в контексте-источнике SrcDC; Rop - код растровой операции, определяющий способ комби- нации точек исходного растрового изображения в SrcDC, растрового изображения в DestDC и заполнителя кисти в DestDC. Возможные значения параметра Rop приведены в табл. 5.7. Поясним используе- мые в таблице имена: FinalPixel означает цвет результирующего пиксела; SrcPixel - цвет исходного пиксела в контексте-источнике; DestPixel - цвет исходного пиксела в контексте-приемнике; BrushPixel - цвет исходного пиксела кисти в контексте-приемнике. Параметры DestDC и SrcDC могут ссылаться и на виртуальные, и на физические контексты устройств, что позволяет переносить изо- бражение не только из памяти на экран, но и с экрана в память, а так- же из памяти в память. В приведенном выше примере функция BitBlt копирует растровое изображение с координатами (0, 0) и размером 64x64 из контекста- источника MemDC в контекст-приемник DC. В принимающем кон- тексте битовое изображение помещается в точку (10, 10). Операция простого копирования задается значением SrcCopy в параметре Rop. Если при переносе требуется увеличение или уменьшение растро- вого изображения, вместо BitBlt используется функция StretchBlt(DestDC: HDC; X, Y, Width, Height: Integer; SrcDC: HDC; XSrc, YSrc, SrcWidth, SrcHeight: Integer; Rop: Longint): Bool; Параметры функции эквивалентны параметрам BitBlt, за исключе- нием двух: SrcWidth и SrcHeight задают начальные размеры растрово-
Гпава 5. Ресурсы Windows 211 го изображения в контексте-источнике SrcDC. Если параметр Width равен SrcWidth и параметр Height равен SrcHeight, то функция StretchBlt работает так же, как и BitBlt. Задавая различные по знаку значения для Width и SrcWidth, а также для Height и SrcHeight, можно создавать зеркальные образы, поворачивая растровое изображение соответственно вокруг оси X и Y. Табл. 5.7. Коды растровых операций для функций BitBlt и StretchBlt Код Описание Blackness Whiteness SrcCopy SrcPaint SrcAnd Srclnvert SrcErase NotSrcCopy NotSrcErase Dstlnvert MergePaint MergeCopy PatCopy PatPaint Patinvert Все результирующее изображение черное Все результирующее изображение белое FinalPixel := SrcPixel FinalPixel := SrcPixel or DestPixel FinalPixel := SrcPixel and DestPixel FinalPixel := SrcPixel xor DestPixel FinalPixel := SrcPixel and (not DestPixel) FinalPixel := not SrcPixel FinalPixel := not (SrcPixel or DestPixel) FinalPixel := not DestPixel FinalPixel := (not SrcPixel) or DestPixel FinalPixel := BrushPixel and SrcPixel FinalPixel := BrushPixel FinalPixel := BrushPixel or (not SrcPixel) or DestPixel FinalPixel := BrushPixel xor DestPixel Если бы в предыдущем примере нам потребовалось вывести рас- тровое изображение увеличенным в два раза, то для этого вместо вы- зова BitBlt следовало бы записать: StretchBlt(DC, 10, 10, 128, 128, MemDC, 0, 0, 64, 64, SrcCopy); Уменьшить растровое изображение в два раза можно следующим образом: StretchBlt(DC, 10, 10, 32, 32, MemDC, 0, 0, 64, 64, SrcCopy); При уменьшении размеров растрового изображейия часть графи- ческой информации теряется. С помощью функции SetStretchBltMode программист может управлять уничтожением лишней информации: SetStretchBltMode(DC: HDC; StretchMode: Integer): Integer; где DC - контекст устройства; StretchMode - один из режимов кор- рекции, приведенных в табл. 5.8.
212 Программирование в среде Borland Pascal для Windows Табл. 5.8. Режимы уменьшения размеров растрового изображения Режим Описание BlackOnWhite За счет пикселов белого цвета сохраняются пикселы черного цвета WhiteOnBlack За счет пикселов черного цвета сохраняются пикселы белого цвета ColorOnColor Используется для отображения цветных графических изображений. Эффект зависит от сочетания цветов конкретного битового образа Чтобы определить текущий режим коррекции изображения, надо вызвать функцию GetStretchBltMode(DC: HDC): Integer; где DC - контекст устройства. Возможные значения функции приве- дены в табл. 5.8. Программа SHOWBMP демонстрирует различные способы вывода растрового изображения: program ShowBmp; {$R SHOWBMP.RES} uses WinTypes, WinProcs, OWindows; const idb_Bitmap = 100; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) Bitmap: HBitmap; {отображаемая в окне картинка} constructor lnit(AParent: PWindowsObject; ATitle: PChar); destructor Done; virtual; procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; PShowBmpApp = ATShowBmpApp; TShowBmpApp = object(TApplication) procedure InitMainWindow; virtual; end; { Конструирует оконный объект и загружает из ресурса картинку} constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); Bitmap := LoadBitmap(Hlnstance, PChar(idb_Bitmap)); end; { Удаляет из памяти картинку и разрушает оконный объект } destructor TMainWindow. Done; begin DeleteObject(Bitmap); inherited Done; end;
Гпава 5. Ресурсы Windows 213 { Рисует картинку натуральной величины, уменьшенной и увеличенной } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var MemDC: HDC; OldBitmap: HBitmap; begin MemDC := CreateCompatibleDC(PaintDC); OldBitmap := SelectObject(MemDC, Bitmap); BitBlt(PaintDC, 10,10, 64, 64, MemDC, 0, 0, SrcCopy); SetStretchBltMode(PaintDC, ColorOnColor); StretchBlt(PaintDC, 10,100, 32, 32, MemDC, 0, 0, 64, 64, SrcCopy); StretchBlt(PaintDC, 100,10,128,128, MemDC, 0, 0, 64, 64, SrcCopy); SelectObject(MemDC, OldBitmap); DeleteDC(MemDC); end; { Создает объект главного окна программы } procedure TShowBmpApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Show Bitmap’)); end; var ShowBmpApp: TShowBmpApp; begin ShowBmpApp.lnit(’ShowBmp’); ShowBmpApp. Run; ShowBmpApp. Done; end. В объекте TMainWindow объявлено поле Bitmap, которое хранит дескриптор загружаемого из ресурса растрового изображения. За- грузку выполняет конструктор TMainWindow.Init с помощью функ- ции LoadBitmap. В деструкторе TMainWindow.Done растровое изо- бражение уничтожается с помощью DeleteObject. Метод Paint выводит битовый образ трижды: натуральной величины (с помощью функции BitBlt), уменьшенным и увеличенным в два раза (с помощью функции StretchBIt). Перед выводом уменьшенного изображения вызывается функция SetStretchBItMode для установки режима коррекции ColorOnColor. 5.8.4. РИСОВАНИЕ РАСТРОВЫХ ИЗОБРАЖЕНИЙ Любое рисование в контексте виртуального устройства приводит к изменению установленного в нем битового образа. Поэтому картинки можно не только отображать, но и рисовать с помощью функций GDI. Если в программе требуется осуществлять продолжительное и час- тое рисование, то лучше делать это в контексте виртуального устрой- ства, т.е. в памяти, а потом скопировать полученный образ на экран.
214 Программирование в среде Borland Pascal для Windows В итоге за счет определенных затрат оперативной памяти значительно повышается скорость графического вывода. Так, кстати, и работают многие графические программы. Рассмотрим подробно процедуру создания и раскраски растрового изображения. Следующий фрагмент программы создает новое растровое изо- бражение и устанавливает его поверхностью рисования в контексте виртуального устройства. { Получаем контекст дисплея для окна HWindow} DC := GetDC(HWindow); { Создаем вспомогательный контекст устройства с поверхностью рисования в оперативной памяти } MemDC := CreateCompatibleDC(DC); { Создаем растровое изображение в формате, совместимом с оконным контекстом дисплея } Bitmap := CreateCompatibleBitmap(DC, 256, 192); { Устанавливаем во вспомогательном контексте устройства новую поверхность рисования - Bitmap } OldBitmap := SelectObject(MemDC, Bitmap); Сначала для окна HWindow запрашивается контекст устройства и сохраняется в переменной DC. Затем создается совместимый с DC контекст виртуального устройства, который запоминается в перемен- ной MemDC. Следующим шагом создается новое растровое изобра- жение. Для этого вызывается функция CreateCompatibleBitmap, в ко- торую передаются контекст устройства и размеры битового образа. В примере CreateCompatibleBitmap создает новое аппаратно-зависимое растровое изображение шириной 256 и высотой 192 пиксела, совмес- тимое по формату с аппаратным контекстом устройства DC. Полу- ченный дескриптор помещается в переменную Bitmap. После этого растровое изображение выбирается в качестве поверхности отобра- жения вспомогательного контекста MemDC. Ранее выбранный бито- вый образ запоминается в переменной OldBitmap. Итак, растровое изображение создано и подготовлено для рисова- ния. Пока оно содержит “мусор” - пикселы изображения имеют слу- чайные цвета. Вот почему следующим действием, обычно выполняе- мым в таких случаях, является закрашивание битового образа неко- торым цветом. Для заливки областей изображения служит функция P^tBlt(DC: HDC; X, Y, Width, Height: Integer; Rop: Longint): Bool; где DC - контекст устройства; X, Y - координаты левого верхнего угла заполняемого прямоугольника; Width, Height - ширина и высота заполняемого прямоугольника; Rop - растровая операция, опреде- ляющая способ комбинации заполнителя кисти с уже существующим в контексте устройства изображением. Возможные значения параметра
Глава 5. Ресурсы Windows 215 Rop составляют подмножество растровых операций для функций BitBlt и StretchBIt (табл. 5.9). Табл. 5.9. Коды растровых операций для функции Pat Bit Код Описание Blackness Все результирующее изображение черное Whiteness Все результирующее изображение белое Dstlnvert FinalPixel := not DestPixel PatCopy FinalPixel := BrushPixel PatPaint FinalPixel := BrushPixel or DestPixel Patinvert FinalPixel := BrushPixel xor DestPixel Пояснение к табл. 5.9: FinalPixel означает цвет результирующего пиксела; DestPixel - цвет исходного пиксела в контексте устройства; BrushPixel - цвет пиксела кисти в контексте устройства. Функция PatBlt закрашивает прямоугольную область с помощью текущей кисти контекста устройства. Способ закрашивания определя- ется кодом растровой операции. Например, в следующей строке все пространство растрового изображения заполняется черным цветом: PatBlt(MemDC, 0, 0, 256,192, Blackness); Теперь в контексте отображения MemDC можно рисовать. В каче- стве примера нарисуем 100 случайных отрезков. Цвет каждого отрез- ка тоже случаен: for I := 1 to 100 do begin Pen := CreatePen(ps_Solid, 4, RGB(Random(256), Random(256), Random(256))); OldPen := SelectObject(MemDC, Pen); MoveTo(MemDC, Random(bmpWidth), Random(bmpHeight)); LineTo(MemDC, Random(bmpWidth), Random(bmpHeight)); SelectObject(MemDC, OldPen); DeleteObject(Pen); end; Когда изображение в памяти сформировано, во вспомогательном контексте MemDC нужно восстановить прежний битовый образ, сам контекст - удалить, а ассоциированный с окном контекст дисплея - освободить: SelectObject(MemDC, OldBitmap); DeleteDC(MemDC); ReleaseDC(HWindow, DC); В результате всех описанных действий формируется растровое изображение с дескриптором Bitmap.
216 Программирование в среде Borland Pascal для Windows Применение рассмотренной методики демонстрирует программа DRAWBMP: program DrawBmp; uses WinTypes, WinProcs {$ifdef VerQO}, Messages {$endif}, OWindows; const bmpWidth = 256; bmpHeight = 192; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) Bitmap: HBitmap; {рисуемая в окне картинка} procedure SetupWindow; virtual; procedure WMDestroy(var Msg: TMessage); virtual wm_First + wmJDestroy; procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; PDrawBmpApp = ATDrawBmpApp; TDrawBmpApp = object(TApplication) procedure InitMainWindow; virtual; end; { После создания окна рисует в памяти картинку} procedure TMainWindow.SetupWindow; var DC: HDC; MemDC: HDC; OldBitmap: HBitmap; Pen, OldPen: HPen; I: Integer; begin inherited SetupWindow; DC := GetDC(HWindow); MemDC := CreateCompatibleDC(DC); Bitmap := CreateCompatibleBitmap(DC, bmpWidth, bmpHeight); OldBitmap := SelectObject(MemDC, Bitmap); PatBlt(MemDC, 0, 0, bmpWidth, bmpHeight, Blackness); for I := 1 to 100 do begin Pen := CreatePen(ps_Solid, 4, RGB(Random(256), Random(256), Random(256))); OldPen := SelectObject(MemDC, Pen); MoveTo(MemDC, Random(bmpWidth), Random(bmpHeight)); LineTo(MemDC, Random(bmpWidth), Random(bmpHeight)); SelectObject(MemDC, OldPen); DeleteObject(Pen); end; SelectObject(MemDC, OldBitmap); DeleteDC(MemDC); ReleaseDC(HWindow, DC); end;
Гпава 5. Ресурсы Windows 217 { Удаляет картинку при уничтожении окна } procedure TMainWindow.WMDestroy(var Msg: TMessage); begin DeleteObject(Bitmap); inherited WMDestroy(Msg); end; { Копирует картинку из памяти на экран } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var MemDC: HDC; OldBitmap: HBitmap; begin MemDC := CreateCompatibleDC(PaintDC); OldBitmap := SelectObject(MemDC, Bitmap); BitBlt(PaintDC, 20, 20, bmpWidth, bmpHeight, MemDC, 0, 0, SrcCopy); SelectObject(MemDC, OldBitmap); DeleteDC(MemDC); end; { Создает объект главного окна программы } procedure TDrawBmpApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'Draw Bitmap')); end; var DrawBmpApp: TDrawBmpApp; begin DrawBmpApp.lnit('DrawBmp')/ DrawBmpApp. Run; DrawBmpApp. Done; end. 5.8.5. РАСТРОВОЕ ИЗОБРАЖЕНИЕ КАК ЗАПОЛНИТЕЛЬ ФОНА Рассматривая GDI, мы уже затрагивали вопрос создания кисти с за- полнителем в виде растрового изображения. Отметим, что эта воз- можность позволяет заполнять фон окна не только цветом, но и по- вторяющимся рисунком. Примером являются диалоговые окна интег- рированной среды Borland Pascal. Приведенная ниже программа BACKGRND создает окно, в кото- ром фон формируется повторением растрового изображения: BACKGRND.INC const idb_Bitmap = 100; BACKGRND.PAS program Backgrnd; {$R BACKGRND. RES} uses WinTypes, WinProcs, OWindows; {$1 BACKGRND.INC}
218 Программирование в среде Borland Pascal для Windows type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) function GetClassName: PChar; virtual; procedure GetWindowClass(var AWndClass: TWndClass); virtual; end; PBackgrndApp = ATBackgrndApp; TBackgrndApp = object(TApplication) procedure InitMainWindow; virtual; end; { Возвращает имя нового оконного класса } function TMainWindow.GetClassName: PChar; begin GetClassName := ’BackgroundDemo'; end; { Устанавливает в оконном классе новый заполнитель фона } procedure TMainWindow.GetWindowClass(var AWndClass: TWndClass); var Bmp: HBitmap; begin inherited GetWindowClass(AWndClass); Bmp := LoadBitmap(Hlnstance, PChar(idb_Bitmap)); AWndClass.hbrBackground := CreatePattemBrush(Bmp); DeleteObject(Bmp); end; { Создает объект главного окна программы } procedure TBackgrndApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'Background Demo')); end; var BackgrndApp: TBackgrndApp; begin BackgrndApp.lnitfBackgmd'); BackgrndApp.Run; BackgrndApp. Done; end. Для того чтобы окно имело требуемый фон, мы переопределили в оконном классе кисть. Новая кисть создается вызовом функции CreatePatternBrush, в которую передается дескриптор загруженного из ресурса растрового изображения. 5.8.6. РАБОТА С ВМР-ФАЙЛАМИ Хотя Windows не предоставляет функций для считывания и записи растровых изображений, хранящихся непосредственно в ВМР-файлах, в Borland Pascal такая возможность существует. По маршруту EXAMPLES\WIN\PAINT относительно каталога, в котором инстал-
Гпава 5. Ресурсы Windows 219 лирован Borland Pascal, находится файл BITMAPS.PAS. Он представ- ляет собой модуль, в котором определены следующие две функции: LoadBitmapFile(FileName: PChar): HBitmap; где FileName - имя файла, из которого считывается растровое изо- бражение, и StoreBitmapFile(FileName: PChar; НВМ: HBitmap): Integer; где FileName - имя файла, в который записывается растровое изобра- жение; НВМ - дескриптор растрового изображения. Функция LoadBitmapFile считывает растровое изображение из файла с именем FileName и возвращает его дескриптор в памяти. Если при чтении файла возникает ошибка, LoadBitmapFile возвращает значение 0. Функция StoreBitmapFile записывает растровое изображе- ние НВМ в файл с именем FileName. Если запись проходит успешно, StoreBitmapFile возвращает ненулевое значение. Хотя модуль BITMAPS является примером, вы можете использовать его в своих программах для работы сВМР-файлами. Ниже приводится программа SLIDES, которая позволяет просмат- ривать растровые изображения, хранящиеся в BMP-файлах на диске. Загрузка растровых изображений осуществляется с помощью функции LoadBitmapFile. Перед запуском SLIDES.EXE создайте в текущем каталоге тексто- вый файл SLIDES.LST и поместите в него имена предназначенных для пррсмотра BMP-файлов (с указанием маршрута). Каждое имя должно начинаться с новой строки. Просмотр растровых изображений вперед осуществляется по нажатии клавиши PgUp или левой кнопки мыши, а назад- по нажатии клавиши PgDn или правой кнопки мыши. program Slides; W uses WinTypes, WinProcs {$ifdef Ver80}, Messages {$endif}, Objects, OWindows, Strings, Bitmaps; const ListFileName: PChar = ’SLIDES.LST; type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) * FileList: TCollection; { список имен файлов с картинками } Fileindex: Integer; { номер текущего файла картинки } Bitmap: HBitmap; {картинка, загруженная из текущего файла} constructor lnit(AParent: PWindowsObject; ATitle: PChar); destructor Done; virtual; procedure SetupWindow; virtual; procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; procedure NextBitmap(Delta: Integer);
220 Программирование в среде Borland Pascal для Windows procedure WMKeyDown(var Msg: TMessage); virtual wm_First + wm_KeyDown; procedure WMLButtonDown(var Msg: TMessage); virtual wm_First + wm_LButtonDown; procedure WMRButtonDown(var Msg: TMessage); virtual wm_First + wm_RButtonDown; end; PSIidesApp = ATSIidesApp; TSIidesApp = object(TApplication) procedure InitMainWindow; virtual; end; { Конструирует оконный объект и инициализирует его поля } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); FileList.lnit(16, 16); Fileindex := 0; Bitmap := 0; end; { Освобождает память и разрушает оконный объект } destructor TMainWindow. Done; begin FileList.DeleteAII; • FileListDone; if Bitmap <> 0 then DeleteObject(Bitmap); inherited Done; end; { После создания окна зачитывает из файла SLIDES.LST имена файлов картинок и делает текущей первую картинку} procedure TMainWindow.SetupWindow; var F: Text; S: array [0..255] of Char; P: PChar; begin inherited SetupWindow; Assign(F, ListFileName); Reset(F); if lOResult = 0 then begin while not Eof(F) do begin ReadLn(F, S); P := StrNew(S); if P <> nil then FileList.lnsert(P); end; if FileList.Count > 0 then NextBitmap(O) else MessageBox(HWindow, ListFileName, 'File is empty*, mbJconExclamation or mb_Ok); end
Гпава 5. Ресурсы Windows 221 else MessageBox(HWindow, ListFileName, 'File not found', mbJconExclamation or mb_Ok); Close(F); end; { Копирует текущую картинку из памяти на экран } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var MemDC: HDC; R: TRect; begin MemDC := CreateCompatibleDC(PaintDC); SelectObject(MemDC, Bitmap); GetClientRect(HWindow, R); BitBlt(PaintDC, 0, 0, R.Right, R.Bottom, MemDC, 0, 0, SrcCopy); DeleteDC(MemDC); end; {Загружает картинку из следующего файла и делает ее текущей} procedure TMainWindow.NextBitmap(Delta: Integer); var S: PChar; begin if FileList.Count > 0 then begin if Bitmap <> 0 then DeleteObject(Bitmap); lnc(Filelndex, Delta); if Fileindex < 0 then Fileindex := FileList.Count -1; if Fileindex >= FileList.Count then Fileindex := 0; S := FileList.At(Filelndex); SetCaption(S); Bitmap := LoadBitmapFile(S); lnvalidateRect(HWindow, nil, True); end; end; { Реализует просмотр картинок с помощью клавиш PgUp и PgDn ) procedure TMainWindow.WMKeyDown(var Msg: TMessage); begin case Msg.WParam of vk_Prior: NextBitmap(-l); vk_Next: NextBitmap(l); else NextBitmap(l); end; end; { По нажатии левой кнопки мыши загружает следующую картинку} procedure TMainWindow.WMLButtonDown(var Msg: TMessage); begin NextBitmap(l); end; { По нажатии правой кнопки мыши загружает предыдущую картинку} procedure TMainWindow.WMRButtonDown(var Msg: TMessage); begin
222 Программирование в среде Borland Pascal для Windows NextBitmap(-l); end; { Создает объект главного окна программы } procedure TSIidesApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Slides')); end; var SlidesApp: TSIidesApp; begin SlidesApp. I nitfSlides'); SlidesApp.Run; SlidesApp. Done end. Программа SLIDES построена следующим образом. В объекте главного окна TMainWindow существует список имен ВМР-файлов (элемент данных FileList), который организован в виде списка строк. Поле Fileindex хранит индекс текущего файла в списке, a Bitmap - дескриптор загруженного из него растрового изображения. Метод TMainWindow.SetupWindow загружает список имен файлов в коллекцию FileList и устанавливает для просмотра первое растровое изображение. Вывод текущего изображения осуществляется в методе TMainWindow.Paint. Смена изображения происходит по нажатии клавиш PgUp, PgDn или кнопок мыши. Для реакции на клавиатуру и мышь в объекте TMainWindow определены методы ответа на сообще- ния wm_KeyDown, wmJLButtonDown и wm_RButtonDown. Методы WMKeyDown, WMLButtonDown и WMRButtonDown осуществляют продвижение по списку FileList, вызывая метод NextBitmap: procedure TMainWindow.NextBitmap(Delta: Integer); var S: PChar; begin if FileList.Count > 0 then begin if Bitmap <> 0 then DeleteObject(Bitmap); lnc(Filelndex, Delta); if Fileindex < 0 then Fileindex := FileList.Count -1; if Fileindex >= FileList.Count then Fileindex := 0; S := FileList.At(Filelndex); SetCaption(S); Bitmap := LoadBitmapFile(S); lnvalidateRect(HWindow, nil, True); end; end; В параметре Delta метода NextBitmap передается смещение отно- сительно текущего элемента коллекции. Фактически параметр Delta
Гпава 5. Ресурсы Windows 223 содержит приращение индекса Fileindex. Оно может быть как поло- жительным (продвижение вперед), так и отрицательным (продвижение назад). Если коллекция не является пустой, осуществляется продвиже- ние по списку файлов в соответствии с параметром Delta с заменой растрового изображения на новое текущее. При этом элемент данных Fileindex получает новое значение, имя текущего файла помещается в заголовок окна вызовом метода SetCaption, предыдущее растровое изображение удаляется, а из нового файла зачитывается другое изо- бражение. После этого с помощью функции InvalidateRect рабочая область главного окна объявляется недействительной. Это заставляет Windows обновить окно программы посылкой сообщения wm_Paint. 5.9. ТАБЛИЦА СТРОК 5.9.1. ОБЩИЕ СВЕДЕНИЯ Основная причина появления строковых ресурсов - желание сделать более легкой адаптацию приложений к различным применениям и их перевод на разные языки. Если строки определяются внутри исходно- го текста, то при трансляции программы они помещаются непосред- ственно в выполняемый файл. А это создает трудности при переводе отображаемой на экране текстовой информации на другой язык, так как выполняемый файл сложно модифицировать. Если строки сооб- щений и подсказок определяются как ресурсы, то они помещаются в специальную таблицу строк. Таблица строк дописывается вместе с другими ресурсами к выполняемому файлу, но может быть изменена с помощью соответствующих средств, например редактора ресурсов Resource Workshop. Другое преимущество использования ресурсов для хранения строк заключается в экономии памяти. Дело в том, что Windows- приложение может иметь только один сегмент данных (адресуемый регистром DS) объемом не более 64 Кбайт. Причем в одном этом сег- менте размещается еще стек и так называемая локальная динамическая память (она используется планировщиком памяти BPW незаметно для программиста).. Большие программы часто требуют 16-32 Кбайт для своего стека и столько же для локальной динамической памяти. При этом совсем не остается места на глобальные данные (глобальные переменные и константы). Основным потребителем области данных являются строки, причем большинство этих строк используется толь- ко в отдельных режимах работы программы, т.е. редко, например при выводе сообщений об ошибке. Помещая строки в ресурс, мы освобо- ждаем сегмент данных для других переменных и констант и возлагаем на Windows заботу об эффективном использовании оперативной па- мяти при загрузке строк из ресурса.
224 Программирование в среде Borland Pascal для Windows Как уже говорилось, все строки в файле ресурса собираются в таб- лицу. В ЕХЕ-файле может быть только одна таблица строк. Если про- грамма подключает несколько RES-файлов, каждый со своей табли- цей строк, то в результирующем ЕХЕ-файле все таблицы объединяют- ся в одну. Хотя таблица строк может иметь объем до 64 Кбайт, в опе- ративной памяти всегда присутствует только ее часть, а требуемые программе строки подгружаются по мере надобности. Этим строко- вые ресурсы напоминают оверлеи. Каждая строка может иметь длину до 255 символов. Все строки хранятся в формате ASCIIZ и идентифицируются в таблице номером, который может находиться в диапазоне от 0 до 65534. Нужно прини- мать во внимание, что Windows всегда загружает строки группами по 16 элементов: с 0-й по 15-ю, с 16-й по 31-ю и т.д. Например, если за- гружается строка с номером 40, то в память из ресурса переносятся строки с 32-й по 47-ю. Поэтому для улучшения использования опера- тивной памяти таблицу строк следует организовывать так, чтобы в “пакете” из 16 элементов находились строки, которые используются совместно. Неиспользуемые номера не занимают места в таблице строк, поэтому строки можно разбивать на поддиапазоны. Например, сообщения об ошибках могут начинаться с 0, подсказки - с 1000, строки блока списка - с 2000 и т.д. 5.9.2. СОЗДАНИЕ ТАБЛИЦЫ СТРОК Чтобы в Resource Workshop открыть редактор таблицы строк, нужно выбрать в меню команду Resource-New и в диалоговом окне New resource указать тип ресурса STRINGTABLE. Редактор таблицы строк имеет три колонки (рис. 5.9). В левую (ID Source) вписывается номер или константный идентификатор стро- ки в таблице. В среднюю колонку (ID Value) помещается числовое значение идентификатора. Эту колонку непосредственно редактиро- вать нельзя, она является информационной. В правую колонку (String) записывается строка. Для того чтобы добавить новый элемент в таб- лицу, нужно нажать клавишу Ins или выбрать команду меню Stringtable-New item. Удаление строк из таблицы осуществляется по команде меню Stringtable-Delete item. При вводе строки можно использовать управляющие символы, на- пример: \t - символ табуляции; \п - символ перевода строки; \г - символ возврата каретки; \\ - символ обратной косой черты ’V; \” - символ двойной кавычки
Гпава 5. Ресурсы Windows 225 Рис. 5.9. Редактор таблицы строк в Resource Workshop Допустимы также обратная наклонная черта \, за которой следует восьмеричный код символа, а также последовательность \х, за кото- рой идет шестнадцатеричный код символа. Например, символ перево- да строки \п может быть задан как \012 или \хОА. 5.9.3. ЗАГРУЗКА СТРОК Использование строковых ресурсов рассмотрим на примере вывода сообщений об ошибках. Для этого вернемся к программе ERRMSG (см. 3.4), которая демонстрирует обработку ошибок в OWL. Напомним, что в объекте TApplication есть метод Error, который вызывается при обнаружении средствами OWL ошибки. По умолча- нию он выводит в диалоговом окне, отображаемом с помощью функ- ции MessageBox, код ошибки и предлагает на выбор продолжить вы- полнение программы или завершить ее. В наследнике TApplication мы перекрываем этот метод, чтобы сделать сообщения об ошибке более информативными. Вместо номера ошибки программа ERRMSG вы- водит в диалоговом окне вполне понятное сообщение, «которое сопро- вождается предложением завершить приложение. Обработку ошибок мы рассматривали в контексте приложения, допускающего исполне- ние только одного своего экземпляра. Переработаем программу ERRMSG таким образом, чтобы строки сообщений об ошибках загружались из ресурса. Загрузку строк осу- ществляет функция LoadString, которая объявлена в WINPROCS сле- дующим образом: 8 Зак. 1049
226 Программирование в среде Borland Pascal для Windows LoadString(Instance: THandle; ID: Word; Buffer: PChar; BufferMax: Integer): Integer; где Instance - дескриптор приложения, загружающего строку; ID - числовой идентификатор строки в таблице; Buffer - приемный буфер для нуль-терминированной строки; BufferMax - максимальный раз- мер буфера. Если строка не помещается в буфер, то она отсекается. Так как максимальный размер строки составляет 255 символов, для загрузки всей строки в этом параметре нужно передавать значение 256, учитывая нуль-терминатор. Функция возвращает число скопиро- ванных в буфер символов. Возможный вариант обработчика ошибок, использующего LoadString, приведен ниже: procedure TStrTableApp.Error(ErrorCode: Integer); var Buf: array [0..271] of Char; begin StrCopy(Buf, 'Unknown error'); if (ErrorCode >= em_Sparelnstance) and (ErrorCode <= emJnvalidWindow) then LoadString(Hlnstance, Abs(ErrorCode), Buf, 256); StrCat(Buf, #13#10'Exit program?'); if MessageBox(0, Buf, 'Application Error', mbJconStop + mb_YesNo) = idYes then Halt; end; В методе TStrTableApp.Error локальный буфер Buf имеет объем, достаточный для размещения текста сообщения об ошибке и строки #13#10'Exit program?' (именно поэтому он занимает 272 символа). Сначала в буфер помещается сообщение 'Unknown error'. Если код ошибки находится в допустимом диапазоне, то строка сообщения загружается в буфер из ресурса с помощью функции LoadString. При этом номер строки в таблице вычисляется как абсолютное значение параметра ErrorCode, содержащего код ошибки. При загрузке строки ее длина еще не известна, поэтому в качестве размера приемного бу- фера указывается максимальное значение 256. Перед вызовом функ- ции MessageBox к сообщению об ошибке добавляется строка 'Exit program?', отделенная от него символами перевода строки (#13#10). Если MessageBox возвращает idYes, это значит, что пользователь ре- шил завершить приложение, и происходит выход из программы опе- ратором Halt. Приведем полный текст программы STRTABLE: program StrTable; {$R STRTABLE.RES} uses WinTypes, WinProcs, OWindows, Strings; const em_Sparelnstance = -6; { код ошибки лишнего экземпляра }
Глава 5. Ресурсы Windows 227 type PStrTableApp = ATStrTableApp; TStrTableApp = object(TApplication) procedure Initlnstance; virtual; procedure InitMainWindow; virtual; procedure Error(ErrorCode: Integer); virtual; end; { Инициализирует только первый экземпляр приложения } procedure TStrTableApp.lnitlnstance; begin if HPrevInst = 0 then inherited Initlnstance else Status := em_Sparelnstance; end; { Создает объект главного окна программы } procedure TStrTableApp. InitMainWindow; begin MainWindow := New(PWindow, lnit(nil, 'Single Instance')); end; { Обрабатывает ошибки прикладного объекта } procedure TStrTableApp.Error(ErrorCode: Integer); var Buf: array [0..271] of Char; begin StrCopy(Buf, 'Unknown error'); if (ErrorCode >= em_Sparelnstance) and (ErrorCode <= emJnvalidWindow) then LoadString(Hlnstance, Abs(ErrorCode), Buf, 256); StrCat(Buf, #13#10'Exit program?'); if MessageBox(0, Buf, 'Application Error', mbJconStop + mb_YesNo) = idYes then Halt; end; var StrTableApp: TStrTableApp; begin StrTableApp.lnit('StrTable’); StrTableApp.Run; StrTableApp. Done; end. 5.10. ДИАЛОГОВЫЕ БЛОКИ 5.10.1. ЧТО ТАКОЕ ДИАЛОГОВЫЙ БЛОК Блок диалога - это окно фиксированного размера, содержащее раз- личные управляющие элементы: кнопки, строки редактирования, спи- ски, комбинированные блоки, кнопки с зависимой и независимой фиксацией, редактируемый и нередактируемый текст, группы, линей- ки скроллинга. Примером диалогового блока является окно ввода
228 Программирование в среде Borland Pascal для Windows имени файла в среде Borland Pascal, которое появляется на экране при выборе команды меню File-Open.... Кстати говоря, если выбор пункта меню приводит к отображению окна диалога, то этот пункт принято дополнять тремя точками, как вы только что могли заметить. Диалоговые блоки могут исполняться в одном из двух режимов, модальном и немодальном. Модальные диалоговые окна характеризуются тем, что не позволяют переключаться на другие окна своего приложения до тех пор, пока работа с ними не будет завершена. Этот вид диалоговых блоков ис- пользуется для того, чтобы организовать ввод каких-либо данных как неделимое действие. Например, в Borland Pascal ввод имени загру- жаемого или сохраняемого файла осуществляется в модальном диало- говом окне. Однако модальные диалоговые блоки позволяют пере- ключаться на другие приложения (например, нажатием комбинации клавиш Alt+Tab); при возврате в приложение с открытым модальным окном диалога управление передается диалоговому окну. По согла- шению модальные диалоговые окна имеют широкую рамку, цвет ко- торой совпадает с цветом заголовка. Такую рамку называют модаль- ной. В модальных блоках диалога, как правило, присутствуют кнопки Ок и Cancel; обе завершают исполнение диалогового окна, но с раз- ным эффектом. По нажатии кнопки Ок данные из окна диалога пере- носятся в программу. Кнопка Cancel служит для отмены действия, вызвавшего появление диалогового окна. На практике почти все диа- логовые блоки являются модальными. Немодальные блоки диалога используются для организации ввода данных, не прерывающего выполнение программы. В отличие от мо- дальных немодальные диалоговые окна предоставляют пользователю свободу выбора, позволяя переключаться на другие окна приложения, использовать меню, выполнять другие операции. Внешне они могут отличаться от модальных окон отсутствием характерной для послед- них рамки и наличием кнопки минимизации. Кнопку максимизации в диалоговых блоках вообще не используют, так как окна диалога должны иметь фиксированные размеры. Существует еще один тип диалоговых блоков - так называемые системно-модальные окна диалога. Они выглядят так же, как и мо- дальные, но не позволяют переключаться ни на какие другие окна, даже на окна других приложений. Системно-модальные окна диалога используются в особых случаях, когда пользователь должен ответить на вопрос прежде, чем продолжить работу с каким-нибудь приложе- нием. Принципиальное различие между модальными и немодальными диалоговыми блоками состоит в механизме обработки сообщений Windows. Немодальные блоки диалога получают свои сообщения так же, как и обычные окна, через цикл обработки сообщений приклад-
Глава 5. Ресурсы Windows 229 ной программы. Модальные диалоговые блоки получают сообщения, минуя цикл прикладной программы; для них Windows организует отдельный цикл обработки сообщений. Следует помнить, что и блоки диалога, и управляющие элементы являются окнами, поэтому им присущи все атрибуты и свойства обычных окон. Все они описываются прямоугольной областью на экране, их взаимоотношения подчиняются правилу “родители-дети”. Управляющие, элементы являются дочерними окнами своего роди- тельского диалогового окна. В свою очередь блоки диалога чаще все- го являются дочерними окнами главного окна программы. В чем же состоит основное отличие диалоговых блоков от обыч- ных окон? На этот вопрос можно ответить так. Блок диалога является специализированным окном, атрибуты и управляющие элементы ко- торого задаются в файле"ресурсов Windows. Рассмотрим, как созда- ются ресурсы диалоговых блоков. 5.10.2. РАЗРАБОТКА ДИАЛОГОВЫХ БЛОКОВ В текстовом файле описания ресурсов существует раздел, называемый DIALOG. Он содержит описание атрибутов диалогового окна и всех его элементов управления. Процесс описания блока диалога в RC- файле настолько трудоемок, что без редактора ресурсов в этом деле просто не обойтись. Связано это с тем, что для окна диалога и его дочерних управляющих элементов нужно задавать координаты и раз- меры, а рассчитывать их “вручную” крайне утомительно. Здесь Resource Workshop приходится как нельзя кстати. Приступая к разработке диалогового блока, нужно создать в Resource Workshop новый или открыть уже существующий проект, выбрав в меню команду Resource-New... и указав тип ресурса DIALOG. В результате этих действий открывается редактор с пустым окном диалога. Управляющие элементы помещаются в него с помо- щью команд из меню Control или кнопок инструментальной панели Tools. Для примера мы разработали в Resource Workshop блок диало- га, показанный на рис. 5.10. Resource Workshop позволяет иметь в диалоговом блоке как базо- вые управляющие элементы (табл. 5.10), так и дополнительные управ- ляющие элементы фирмы Borland (табл. 5.11). Классификация элемен- тов управления достаточно условна и основана на их внешнем разли- чии. При более глубоком рассмотрении оказывается, что каждый управляющий элемент принадлежит определенному оконному классу. В Windows существует шесть стандартных оконных классов для эле- ментов управления: Button, ListBox, ComboBox, Edit, Static, ScrollBar. В своих продуктах фирма Borland International добавляет к ним еще пять: BorBtn, BorCheck, BorRadio, BorStatic, BorShade. Во вторых
230 Программирование в среде Borland Pascal для Windows колонках табл. 5.10 и табл. 5.11 для каждого управляющего элемента указывается его оконный класс. Он определяет наиболее общие свой- ства элемента управления. Табл. 5.10. Базовые элементы управления Управляющий элемент Имя оконного класса Описание Push button BUTTON Нажимающаяся кнопка Check box BUTTON Кнопка независимой фиксации Radio button BUTTON Кнопка зависимой фиксации (селективная кнопка) Group box BUTTON Окно группы (группа) List box LISTBOX Окно списка (список) Combo box COMBOBOX Комбинированный блок Edit text EDIT Редактируемый текст Static text STATIC Нередактируемый (статический) текст Icon STATIC Пиктограмма (“иконка”) Frame STATIC Рамка (белая, серая, черная) Rectangle STATIC Прямоугольник (белый, серый, черный) Scroll bar SCROLLBAR Полоса скроллинга: горизонтальная, вер- тикальная Табл. 5.11. Дополнительные элементы управления Borland Управляющий элемент Имя оконного класса Описание Push button BorBtn Нажимающаяся кнопка Check box BorCheck Кнопка независимой фиксации Radio button BorRadio Кнопка зависимой фиксации (селективная кнопка) Static text BorStatic Нередактируемый (статический) текст Bitmap BorStatic Растровое изображение Recessed shade BorShade Утопленный прямоугольник Raised shade BorShade Выступающий прямоугольник Dip BorShade Разделительная канавка (горизонтальная, вертикальная) Bump BorShade Разделительный бортик (горизонтальный, вертикальный)
Глава 5. Ресурсы Windows 231 Рис. 5.10. Окно редактора диалогов Атрибуты диалогового окна и его дочерних окон можно изменять в процессе редактирования. Текущий редактируемый элемент Resource Workshop обводит рамкой. Изменяя размеры этой рамки и перемещая ее по экрану, мы изменяем размеры и координаты редак- тируемого элемента. Чтобы изменить другие атрибуты интерфейсного элемента, нужно выбрать его двухкратным щелчком мыши. Двухкратным щелчком мыши на заголовке окна диалога вызыва- ется редактор атрибутов (рис. 5.11). При определении атрибутов диа- логового окна указываются: заголовок (Caption), имя оконного класса (Class), идентификатор автоматически устанавливаемого в окне меню (Menu), стили оформления диалогового окна (Window type, Frame style, Dialog style), параметры используемого в окне шрифта (Font). Если диалоговому окну указать имя класса BorDlg, то мы получим блок диалога в стиле диалоговых окон редактора Resource Workshop. Обратите внимание на следующую особенность: чтобы диалоговые блоки класса BorDlg выглядели в вашей программе так, как вы того ожидаете, в ней необходимо подключить модуль BWCC (Borland Windows Custom Controls). Это интерфейсный модуль библиотеки управляющих элементов, используемых в продуктах фирмы Borland. Управляющие элементы из BWCC.DLL работают так же, как и стан- дартные, но выглядят более привлекательно.
232 Программирование в среде Borland Pascal для Windows Рис. 5.11. Диалог установки атрибутов окна Редактируя атрибуты управляющего элемента, мы указываем его заголовок, числовой идентификатор внутри диалогового окна, пара- метры стиля, другие атрибуты (так, на рис. 5.12 приведен внешний вид диалога установки атрибутов кнопки). Если окно диалога в ресурсе может идентифицироваться строкой или номером, то для управляю- щего элемента идентификатор может быть только- числовым. Этим диалоговые ресурсы напоминают ресурсы меню (для идентификации всего меню может использоваться строка или число, но команды ме- ню идентифицируются только по номеру). В пределах диалогового блока идентификаторы управляющих элементов должны быть уни- кальными. Им следует назначать небольшие номера, например 101, 102 и т.д. Элементам нередактируемого текста, которые не задержи- вают фокус ввода, лучше всего назначать идентификатор -1. В атрибутах многих управляющих элементов можно задать пара- метр Tab Stop. Он показывает, будет ли данный элемент получать управление при перемещении фокуса ввода между элементами диало- гового окна. Перемещение фокуса ввода осуществляется нажатием клавиши Tab (в обратном направлении - нажатием Shift+Tab). В Resource Workshop существуют различные вспомогательные средства, облегчающие редактирование диалоговых блоков. Напри- мер, для округления координат и размеров управляющих элементов можно установить сетку (grid); для этого выбирается команда меню
Гпава 5. Ресурсы Windows 233 Рис. 5.12. Диалог установки атрибутов кнопки Align-Grid... и в диалоговом окне задаются атрибуты сетки. Чтобы переместить несколько элементов группой, нужно, удерживая клави- шу Shift, отметить каждый из них щелчком мыши; после этого поме- ченные управляющие элементы будут перемещаться одновременно при передвижении любого из них. Быстро изменить порядок управ- ляющих элементов можно перестановкой строк в RC-файле описания ресурсов. Существует также много других полезных возможностей, которые подробно рассматриваются в руководстве по Resource Workshop. В заключение обзора диалоговых ресурсов хотелось бы дать один полезный совет. Блок диалога всегда должен иметь средства “безопасного” завершения своего выполнения. Одним из таких средств является кнопка Cancel, по которой должна происходить от- мена действий, приведших к созданию диалогового окна. В некото- рых случаях эта кнопка может называться иначе, например Close. Кнопка фиксации введенных данных, как правило, называется Ок. Создавая блок диалога, помните о том, что при работа с вашей про- граммой пользователь не должен задумываться над тем, как безопас- но закрыть окно. 5.10.3. ВЫПОЛНЕНИЕ ДИАЛОГОВЫХ БЛОКОВ Диалоговый блок является единственным видом ресурса, для которо- го в OWL определена иерархия классов (рис. 5.13).
234 Программирование в среде Borland Pascal для Windows и TControl (наследник TWindow). TDialog является интерфейсным объектом и модальных, и немодальных окон диалога, a TControl - родоначальником управляющих объектов. В иерархии OWL имеется еще объект TDlgWindow, наследник TDialog, который совмещает свойства диалогового блока и обычного окна. Как блок диалога, объ- ект типа TDlgWindow создается из ресурса. В то же время он регист- рирует оконный класс, в котором можно установить собственный стиль, заполнитель фона, курсор, пиктограмму окна, другие парамет- ры. Чтобы объяснить общую схему создания и выполнения диалогово- го блока, сначала приведем пример, в котором диалоговый блок ис- пользуется для ввода некоторой строки и булевской переменной: type TPerson = record Name: array [0 .. 15] of Char; { имя человека } Married: WordBool; { семейное положение } end; var Dialog: PDialog; {указатель на диалоговый объект } Control: PControl; { вспомогательный указатель } Person: TPerson; { данные пользователя } Result: Integer; {результат исполнения модального диалога } begin { Создаем диалоговый объект } Dialog := New(PDialog, lnit(nil, 'ResourceName')); { Создаем объекты для управляющих элементов, редактирующих данные } Control := New(PEdit, lnitResource(Dialog, 101, SizeOf(Person.Name)); Control := New(PCheckBox, lnitResource(Dialog, 102);
Гпава 5. Ресурсы Windows 235 { Устанавливаем указатель буфера передачи на данные пользователя } Dialog A.TransferBuffer := ©Person; { Если диалог модальный, запускаем его и уничтожаем объект } Result := Dialogs Execute; Dispose(Dialog, Done); { Если диалог немодальный, создаем и отображаем его } { Dialog*. Create;} { Dialog* Show(sw_ShowNormal);} end; На основе данного примера сформулируем общий алгоритм: • создается экземпляр диалогового объекта; • если диалог организуется для ввода каких-нибудь данных, то для каждого элемента управления, принимающего и возвращающего дан- ные, создается интерфейсный объект; • указателю TransferBuffer диалогового объекта присваивается ад- рес буфера данных; • если организуется модальный диалог, то блок диалога загружа- ется из ресурса и запускается на выполнение вызовом у диалогового объекта метода Execute; • если организуется немодальный диалог, то блок диалога загру- жается из ресурса вызовом у диалогового объекта метода Create и отображается с помощью метода Show; • после выполнения модального диалога диалоговый объект унич- тожается. Создание диалогового объекта осуществляется через вызов его конструктора: TDialog.Init(AParent: PWindowsObject; AName: PChar); где A Parent - указатель на родительский объект, для которого созда- ваемый объект выступает дочерним; AName - идентификатор диало- гового блока в файле ресурса. Когда объект диалога готов, создаются управляющие объекты тех элементов управления, которые редактируют данные. Нужно учиты- вать, что управляющими элементами владеет окно диалога, а диало- говый OWL-объект - только управляющими объектами. При загрузке из ресурса и запуске диалогового окна вместе с ним всегда загружа- ются и отображаются дочерние управляющие элементы, независимо от того, создаем мы для них интерфейсные объекты или нет. Спраши- вается, для чего нужны управляющие объекты OWL? Если управляю- щие элементы не изменяют в своей работе никаких данных, то интер- фейсные объекты для них действительно не нужны. Например, для кнопок редко создаются интерфейсные объекты. Однако для строк редактирования это необходимо. Создавая для элемента управления интерфейсный объект, мы задействуем для него реализованный в OWL механизм передачи данных. Этот механизм обеспечивает авто-
236 Программирование в среде Borland Pascal для Windows магическое заполнение окна диалога исходной информацией перед началом его работы и считывание результатов после завершения. Механизм передачи данных учитывает особенности управляющих элементов и поэтому рассмотрен в гл. 6. Как же создаются управляю- щие объекты? Взгляните на диаграмму оконных классов (см. рис. 5.13). На ней видно, что базовый управляющий объект TControl является наслед- ником объекта TWindow. Интерфейсные объекты, порожденные от TWindow, имеют, кроме конструктора Init, еще один конструктор: TWindow.InitResource(AParent: PWindowsObject; ResourcelD: Word); где A Parent - указатель на родительский объект, для которого созда- ваемый объект выступает дочерним; ResourcelD - целочисленный идентификатор управляющего элемента в диалоговом блоке. Конструктор InitResource предназначен для создания интерфейс- ного объекта, в котором интерфейсный элемент загружается из ресур- са. В связи с этим он не требует указания параметров будущего окна. В него передается только идентификатор управляющего элемента в ресурсе. Остальные атрибуты оконного объекта конструктор InitResource инициализирует нулем. В управляющих объектах, произ- водных от TControl, конструктор InitResource часто переопределяется й имеет большее число параметров. В процессе создания объект запоминает, каким конструктором он был создан. Когда в результате вызова у диалогового объекта метода Create или Execute из ресурса загружается блок диалога, у каждого управляющего объекта вызывается метод Create. В методе Create управляющий объект анализирует, каким конструктором он был соз- дан: InitResource или Init. Если конструктором InitResource, то управ- ляющий объект находит в диалоговом окне по идентификатору ресур- са свой управляющий элемент и устанавливает с ним связь. При этом задействуется механизм передачи данных между управляющим объек- том и его владельцем. Если управляющий объект создавался конст- руктором Init, то в методе Create он создает свой управляющий эле- мент с самого начала, зная все его атрибуты. Если вы внимательно посмотрите на приведенный выше про- граммный фрагмент, то обнаружите еще одну особенность создания управляющих объектов. Она заключается в том, что их адреса, воз- вращаемые функцией распределения памяти, нигде не используются. Дело в том, что при конструировании интерфейсных объектов им сообщается адрес родительского объекта, а они сами умеют добавлять себя в список дочерних окон своего родителя. Этим свойством обла- дает любой оконный объект, порожденный от TWindowsObject. Когда управляющие объекты созданы, указателю TransferBuffer диалогового объекта присваивается адрес буфера передачи данных,
Гпава 5. Ресурсы Windows 237 т.е. области оперативной памяти, из которой извлекаются и в кото- рую помещаются редактируемые данные. Обычно в качестве буфера используется запись, в которой поля располагаются в порядке следо- вания элементов управления внутри окна диалога. Тип каждого поля определяется размером и типом данных, обрабатываемых соответст- вующим управляющим элементом. При пересылке данных каждый управляющий объект “откусывает” от буфера столько, сколько счита- ет нужным, и передает в свой управляющий элемент. Обратите вни- мание на то, что поле TransferBuffer и методы работы с ним объявле- ны в TWindowsObject, поэтому механизм передачи данных можно использовать в любых окнах, не только диалоговых. После того, как созданы управляющие объекты и установлен бу- фер передачи данных, все готово к созданию самого окна диалога. В зависимости от режима работы диалогового блока (модальный или немодальный), следует вызывать тот или иной метод объекта. Для создания модального диалога в диалоговом объекте существует метод Execute. Чтобы организовать немодальный диалог, нужно вызвать подряд методы Create и Show (как при работе с обычными окнами). И в том, и в другом случае окно диалога создается из ресурса, отобража- ется на экране и начинает обработку сообщений. Различие состоит только в режиме его работы. Профессиональным программистам будет интересно узнать, что методы Create и Execute никак не связаны между собой, более того, вызывают для своих целей разные функции Windows. Дело в том, что модальность и немодальность диалоговых блоков поддерживается системой Windows, а не библиотекой OWL. Для создания немодаль- ного окна диалога метод Create вызывает API-функцию CreateDialogParam. Модальный диалог создается из метода Execute другой функцией Windows - DialogBoxParam. Объект TApplication предоставляет улучшенный способ создания и запуска окон в виде двух своих методов: MakeWindow и ExecDialog. Для создания любого окна, в том числе и немодального окна диалога, вместо метода Create оконного объекта лучше использовать метод MakeWindow объекта TApplication, например: Dialog := Application*.MakeWindow(Dialog); if Dialog <> nil then DialogA.Show(sw_ShowNormal); Для создания модального окна диалога вместо* метода Execute диалогового объекта лучше использовать метод ExecDialog объекта TApplication, например: Result := Application*.ExecDialog(Dialog); Будьте внимательны при работе с методами MakeWindow и ExecDialog! Перед своим завершением ExecDialog удаляет диалоговый
238 Программирование в среде Borland Pascal для Windows объект, считая, что он был распределен в динамической памяти. MakeWindow делает такое же предположение, разрушая оконный объект при неудачном создании окна. Отсюда следует два вывода: 1) объекты диалога необходимо создавать в динамической памяти; 2) их не нужно удалять после выполнения. 5.10.4. ВЗАИМОДЕЙСТВИЕ С ЭЛЕМЕНТАМИ УПРАВЛЕНИЯ При воздействии на элементы управления объекту окна диалога по- сылаются сообщения, которые можно перехватить, определив в на- следнике TDialog методы реакции на них. Методы ответа на сообще- ния от управляющих элементов должны иметь индекс диспетчирова- ния в диапазоне от idJFirst до id_First + S0FFF. Объект TDialog уже умеет реагировать на сообщения от двух стандартных управляющих элементов, кнопок Ок и Cancel, которые имеют зарезервированные идентификаторы id_Ok и id_Cancel соответственно. Ниже приведены другие предопределенные идентификаторы, объявленные в модуле WINTYPES: id_Ok =1; id_Cancel =2; id_Abort = 3; id_Retry = 4; idjgnore = 5; id.Yes = 6; id_No = 7; Методы ответа на сообщения от управляющих элементов с иден- тификаторами id_Ok и id_Cancel завершают работу диалогового бло- ка, причем по сообщению от кнопки Ок данные из окна диалога пере- писываются в буфер передачи TransferBuffer. В своей программе вам может понадобиться завершить диалог по сообщениям от других управляющих элементов, например от кнопки Close. Для этого в объ- екте TDialog объявлен метод TDialog.EndDlg(ARetValue: Integer); virtual; где ARetValue - код завершения диалога. Метод EndDlg используется для завершения работы только мо- дальных диалоговых блоков. Чтобы закрыть немодальное диалоговое окно, нужно вызывать метод CloseWindow. Например, реакция на нажатие кнопки Close может выглядеть так: procedure TMyDialog.lDCIose(var Msg: TMessage); begin if IsModal then EndDlg(id_Close) else CloseWindow; end;
Гпава 5. Ресурсы Windows 239 Значение, передаваемое процедуре EndDlg, возвращается как ре- зультат работы метода Execute при завершении модального диалого- вого блока. По соглашению это значение должно быть идентифика- тором управляющего элемента, закрывшего окно диалога. Часто требуется, чтобы элементы управления работали согласо- ванно. Примером является диалоговое окно открытия файла. При перемещении строки выбора в блоке списка Files имя “текущего” фай- ла отображается в строке редактирования File name. Чтобы организо- вать такое взаимодействие, нужно знать особенности каждого управ- ляющего элемента и методы работы с ним. Работа с управляющими элементами - это обширная тема, которой мы посвятили гл. 6. 5.10.5. ДИАЛОГОВОЕ ОКНО “ABOUT” Построение собственных диалоговых блоков можно пояснить на примере создания окна About. В нем обычно содержится полное на- звание .программы, номер версии, авторские права, отличительный знак продукта в виде пиктограмму. Не будем утомлять вас описанием того, как мы разрабатывали в Resource Workshop ресурс диалогового блока About. Взгляните на рис. 5.10, на нем показан окончательный вид диалога About в редак- торе ресурсов. Кроме окна диалога, мы создали меню Help с пунктом About... и поместили его в тот же файл ресурса. Вывбд диалогового окна с информацией о программе выглядит так: ABOUTBOX.INC const idm_Menu = 100; idd_AboutBox = 100; idi_Aboutlcon = 100; cm_About = 999; ABOUTBOX.PAS program AboutBox; {$R ABOUTBOX RES} uses WinTypes, WinProcs, OWindows, ODialogs; {$1 ABOUTBOX. INC} type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) $ constructor lnit(AParent: PWindowsObject; ATitle: PChar); procedure CMAbout(var Msg: TMessage); virtual cm_First + cm_About; end; PAboutBoxApp = ATAboutBoxApp; TAboutBoxApp = object(TApplication) procedure InitMainWindow; virtual; end;
240 Программирование в среде Borland Pascal для Windows { Конструирует оконный объект и загружает из ресурса меню } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); Attr.Menu:= LoadMenu(Hlnstance, PChar(idm_Menu)); end; {По.команде меню Help-About... открывает окно диалога ”About”} procedure TMainWindow.CMAbout(var Msg: TMessage); begin ApplicationA.ExecDialog(New(PDialog, lnit(@Self, PChar(idd_AboutBox)))); end; { Создает объект главного окна программы } procedure TAboutBoxApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'About Box Demo')); end; var AboutBoxApp: TAboutBoxApp; begin AboutBoxApp. Init('AboutBox'); AboutBoxApp. Run; AboutBoxApp. Done; end. Загрузка меню idm_Menu выполняется в конструкторе Init главно- го окна, а обработка команды cm_About - в методе CMAbout. Метод CMAbout создает и исполняет модальное окно диалога, вызывая для этого метод ExecDialog прикладного объекта. Значение @Self, переда- ваемое в качестве родителя при вызове конструктора, указывает, что диалоговый блок About будет дочерним окном главного окна про- граммы. Как можно заметить, объект создается в динамической памя- ти, но не удаляется. Удаление диалогового объекта выполняется ав- томатически в ExecDialog. Это очень удобно, так как указатель на объект не нужно сохранять во временной переменной. 5.10.6. СТАНДАРТНЫЕ ДИАЛОГОВЫЕ ОБЪЕКТЫ OWL В состав OWL входит модуль OSTDDLGS (STanDard DiaLoGS), в котором определены два стандартных класса диалоговых объектов: TInputDialog и TFileDialog. Для работы этих объектов модуль OSTDDLGS подключает файл ресурса OSTDDLGS.RES, который присоединяется к программе на этапе компоновки автоматически, если в операторе uses указан OSTDDLGS. Объект TInputDialog предназначен для ввода в небольшом диало- говом окне текстовой строки. Внешний вид диалогового окна, кото- рое создает TInputDialog, приведен на рис. 5.14. Организовать ввод строки в таком окне очень просто: нужно создать экземпляр TInputDialog и передать его в метод ExecDialog объекта Application.
Глава 5. Ресурсы Windows 241 Рис. 5.14. Окно объекта TInputDialog Конструктор Init объекта TInputDialog имеет следующий формат: TInputDialog.Init(AParent: PWindowsObject; ACaption, APrompt, ADefText PChar; TextSize: Integer); где A Parent - адрес родительского оконного объекта, который созда- ет данный диалог; ACaption - заголовок диалогового окна; APrompt - строка приглашения (она поясняет, что должен ввести пользователь в диалоговом окне); ADefText - заполняемый пользователем текстовый буфер; A TextSize - размер текстового буфера. При создании окна диалога буфер ADefText помещается в строку редактирования, поэтому его всегда нужно инициализировать (хотя бы пустой строкой). Размер буфера, передаваемый в ATextSize, дол- жен включать нуль-терминатор. Например, если текстовый буфер объявлен так: { адрес объекта, создающего диалог } { заголовок диалогового окна } { подсказка перед строкой ввода } { буфер ввода } { размер буфера } Buffer: array [0 .. N] of Char; то в параметр ATextSize конструктора лучше всего передать SizeOf(Buffer). Использование объекта TInputDialog в программе мо- жет быть следующим: if ApplicationA.ExecDialog(New(PlnputDialog, Init( @Self, 'Input Dialog', 'Enter your name', Buffer, SizeOf(Buffer) ))) = id_Ok then begin {Действия при нажатии кнопки Ok } end; о Если пользователь в диалоге нажимает кнопку Ок, то текст из строки редактора возвращается в буфер. Если нажимается кнопка
242 Программирование в среде Borland Pascal для Windows Cancel, то буфер не изменяется. Диалоговое окно, создаваемое объек- том TInputDialog, может исполняться и в модальном, и в немодаль- ном режимах. Если диалог является немодальным, то кнопки Ок и Cancel не закрывают окно и не изменяют буфер ввода. Одно из самых распространенных применений диалоговых блоков (о котором вы, вероятно, уже подумывали) - ввод имени загружаемо- го или сохраняемого файла. Диалог ввода имени файла обычно созда- ется при выборе в меню команд Open... и Save as... (рис. 5.15). Он по- зволяет не цросто вводить имя файла в строке редактора, а выбирать его, перемещаясь по дереву каталога и переключаясь между логиче- скими дисками. Рис, 5,15, Окно объекта TFileDialog С помощью объекта TFileDialog создать такой диалоговый блок очень просто. Нужно подготовить буфер для имени файла и передать его в конструктор TFileDialog. Буфер имени файла следует объявить так: FilePath: array [0 .. fsPathName] of Char; где константа fsPathName объявлена в WINDOS и равна 79. Конструктор TFileDialog.Init имеет следующий формат: TFileDialog.Init(AParent: PWindowsObject; AName, AFilePath: PChar); где AParent - адрес родительского оконного объекта, который созда- ет данный диалог; AName - идентификатор диалогового окна в файле ресурса; AFilePath - буфер для имени файла. Идентификатор диалого-
Глава 5. Ресурсы Windows 243 вого окна в файле ресурса является псевдоуказателем. В него нужно передавать PChar(sd_FileOpen) для получения имени загружаемого файла и PChar(sd_FileSaye) для получения имени сохраняемого файла. Диалоги ввода имени файла принято делать модальными, поэтому для создания окна нужно вызывать ExecDialog объекта Application. Например, получение в программе имени загружаемого файла выгля- дит приблизительно так: if ApplicationA.ExecDialog(New(PFileDialog, Init( @Self, { адрес объекта, создающего диалог } PChar(sd__FileOpen), { диалог загрузки файла } FilePath { буфер имени файла } ))) = id_Ok then begin { Открываем выбранный файл } end; Если в результате диалога возвращается id_Ok, то файл выбран и можно попытаться его открыть. Использование объектов TInputDialog и TFileDialog демонстриру- ет следующая программа: STDDIAL.INC const idm_Menu = 100; cmjnput =101; STDDIAL.PAS program StdDial; {$R STDDIAL.RES} uses WinTypes, WinProcs, WinDos, Strings, OWindows, OStdDIgs, BWCC; {$1 STDDIAL.INC} type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) constructor lnit(AParent: PWindowsObject; ATitle: PChar); procedure CMInput(var Msg: TMessage); virtual cm_First + cmjnput; procedure CMFileOpen(var Msg: TMessage); virtual cm_First + cm_FileOpen; procedure CMFileSaveAs(var Msg: TMessage); virtual cm_First + cm_FileSaveAs; end; * TStdDialApp = object(TApplication) procedure InitMainWindow; virtual; end; { Конструирует оконный объект и загружает из ресурса меню } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle);
244 Программирование в среде Borland Pascal для Windows Attr.Menu := LoadMenu(HInstance, PChar(idm__Menu)); end; { Организует диалог ввода строки } procedure TMainWindow.CMInput(var Msg: TMessage); var Buffer: array [0..79] of Char; begin StrCopy(Buffer,’’); if ApplicationA.ExecDialog(New(PlnputDialog, lnit(@Self, ’Input Dialog’, 'Enter string’, Buffer, SizeOf(Buffer)))) = id_Ok then begin { Обработка введенной строки } end; end; { Организует диалог ввода имени открываемого файла } procedure TMainWindow.CMFileOpen(var Msg: TMessage); var FilePath: array [O..fsPathName] of Char; begin StrCopy(FilePath, if ApplicationA.ExecDialog(New(PFileDialog, lnit(@Self, PChar(sd_FileOpen), FilePath))) = id_Ok then begin { Открытие файла } end; end; { Организует диалог ввода имени сохраняемого файла } procedure TMainWindow.CMFileSaveAs(var Msg: TMessage); var FilePath: array [O..fsPathName] of Char; begin StrCopy(FilePath, ’’); if ApplicationA.ExecDialog(New(PFileDialog, lnit(@Self, PChar(sd_FileSave), FilePath))) = id_Ok then begin { Сохранение файла } end; end; { Создает объект главного окна программы } procedure TStdDialApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Standard Dialogs’)); end; var StdDialApp: TStdDialApp; begin StdDialApp.lnit(’StdDial’); StdDialApp.Run; StdDialApp. Done; end.
Гпава 5. Ресурсы Windows 245 5.10.7. СТАНДАРТНЫЕ ДИАЛОГОВЫЕ БЛОКИ WINDOWS Кроме стандартных диалоговых объектов OWL, вы можете использо- вать так называемые общеупотребительные блоки диалога (common dialogs) системы Windows. Заголовки функций для создания этих диа- логовых блоков находятся в модуле COMMDLG. В Windows опреде- лены следующие стандартные диалоговые блоки (в скобках приводит- ся имя соответствующей функции Windows, которая создает указан- ный диалоговый блок): • выбор имени открываемого файла (GetOpenFileName); • выбор имени сохраняемого файла (GetSaveFileName); • поиск в тексте строки (FindText); • замена в тексте строки (ReplaceText); • печать файла (PrintDlg); • выбор цвета (ChooseColor); • выбор шрифта (ChooseFont). В библиотеке OWL существуют аналоги только первых пяти диа- логовых блоков. Для выбора шрифта и цвета нужно использовать средства Windows, т.е. функции модуля COMMDLG. Отметим, что стандартные диалоговые блоки Windows предоставляют гораздо более широкие возможности, чем их аналоги из OWL, и не требуют дополнительных программных средств для своей работы. Однако объекты OWL проще в использовании и на экране выглядят красивее (благодаря библиотеке элементов управления BWCC.DLL). Ввиду этого мы приводим описание только функций ChooseColor и ChooseFont для выбора соответственно цвета и шрифта. С остальны- ми функциями создания стандартных диалоговых блоков Windows вы сможете разобраться самостоятельно, если в этом возникнет необхо- димость. Рассмотрим сначала выбор цвета. Как мы уже сказали, для выбора цвета используется' функция ChooseColor (var СС: TChooseColor): Bool; где СС - запись типа TChooseColor, содержащая исходные данные для создания диалогового блока и результат. Функция выполняет все необходимые действия по подготовке и осуществлению диалога, в результате которого пользователь получает возможность выбрать цвет. * Если диалог проходит успешно и пользователь завершает его на- жатием кнопки Ок, то функция ChooseColor возвращает True. Если окно диалога закрывается нажатием кнопки Cancel или по команде Close системного меню либо при выполнении функции ChooseColor происходит ошибка, то возвращается False. Заметим, что причину ошибки, связанную с созданием любого стандартного диалогового
246 Программирование в среде Borland Pascal для Windows блока Windows, можно проанализировать с помощью функции CommDlgExtendedError. Запись TChooseColor объявлена в модуле COMMDLG и имеет сле- дующий вид: TChooseColor = record IStructSize: Longint; hWndOwner: HWnd; hlnstance: HWnd; rgbResult: Longint; IpCustColors: PLongint; Flags: Longint; ICustData: Longint; IpfnHook: function (Wnd: HWnd; Msg, wParam: Word; IParam: Longint): Word; IpTemplateName: PChar; end; Рассмотрим назначение полей: • IStructSize - размер записи TChooseColor в байтах. Это поле за- полняется перед вызовом функции ChooseColor и должно быть равно SizeOf(TChooseColor); • hWndOwner - дескриптор окна, назначаемого владельцем диало- гового блока выбора цвета. Если поле hWndOwner равно 0, то диало- говый блок не имеет владельца; • hlnstance - дескриптор шаблона диалогового блока. Шаблон представляет собой блок данных специального формата, который содержит описание создаваемого окна диалога. Задав шаблон, вы можете получить диалоговый блок выбора цвета, отличный от того, который используется по умолчанию. При этом в поле Flags должен быть установлен флаг cc_EnableTemplateHandle или cc_EnableTemplate. Если ни один из этих флагов не установлен, то значение поля hlnstance игнорируется; • rgbResult - значение цвета в формате RGB. Используется для пе- редачи в диалоговый блок начального цвета и возврата в программу результата выбора пользователя. Начальное значение берется из поля rgbResult, если в поле Flags указан флаг cc_RGBInit; • IpCustColors - указатель на массив из 16 цветов. Каждый цвет задается в формате RGB. В диалоговом окне эти цвета помещаются в ячейки с меткой Custom Colors. Изменение пользователем этих ячеек приводит к изменению соответствующих элементов массива,, адресуе- мого указателем IpCustColors; • Flags - флаги, определяющие способ создания и работы окна вы- бора цвета. Значением поля Flags может быть комбинация констант из табл. 5.12;
Гпава 5. Ресурсы Windows 247 Табл. 5.12. Флаги функции ChooseColor Константа Описание cc_RGBInit Указывает, что значение в поле rgbResult должно использоваться в качестве начального цвета при создании окна диалога cc_FullOpen Указывает, что окно должно сразу открывать- ся полностью, позволяя определить 16 допол- нительных цветов. Если флаг cc_FullOpen не указан, то, чтобы задать дополнительные цвета, пользователь должен нажать в окне кнопку Define Custom Colors... cc_PreventFullOpen Запрещает пользователю определять дополни- тельные цвета в диалоговом окне cc_ShowHelp Помещает в диалоговое окно кнопку Help. При выборе пользователем этой кнопки окну с дескриптором hWndOwner посылается уведо- мительное сообщение ccJEnableHook Разрешает использование функции- перехватчика в поле IpfnHook для обработки сообщений, предназначенных окну диалога cc_EnableTemplate Разрешает использование шаблона с именем IpTemplateName и дескриптором hlnstance для создания диалогового блока cc_EnableTemplateHandle Разрешает использование шаблона с дескрип- тором hlnstance для создания диалогового блока. Значение поля IpTemplateName игнори- руется • ICustData - данные пользователя. При использовании функции- перехватчика IpfnHook создание окна сопровождается передачей этой функции сообщения wm_InitDialog, в котором параметр IParam со- держит указатель на запись типа TChooseCoIor. Для того чтобы с сообщением wm_InitDiaIog в функцию обработки сообщений можно было передать еще какие-либо данные, в записи TChooseCoIor введе- но поле ICustData; • IpfnHook - функция, которая перехватывает сообщения, предна- значенные окну диалога. Чтобы разрешить использование функции- перехватчика, в поле Flags должен быть установлен флаг сс_ЕпаЫеНоок. Если пользовательская функция-перехватчик обрабо- тала сообщение, она должна вернуть ненулевое значение. Если функ- ция возвращает 0, выполняется стандартная обработка сообщения; • IpTemplateName - имя файла ресурсов, в котором содержится шаблон создания диалогового окна для выбора цвета.
248 Программирование в среде Borland Pascal для Windows Для организации выбора цвета по команде меню с кодом cm_Color необходимо в пользовательском оконном объекте объявить два поля и метод реакции на команду cm_Cok>r: TMainWindow = object(TWindow) Color: TColorRef; { редактируемый цвет } CustColors: array [0.. 15] of TColorRef; { дополнительные цвета } procedure CMColor(var Msg: TMessage) virtual cm_First + cm_Color; end; Один из вариантов реализации метода CMColor: procedure TMainWtndow.CMColor(var Msg: TMessage); var ColorRec: TChooseColor; begin { Заполняем запись ColorRec начальными значениями } FillChar(ColorRec, SizeOf(ColorRec), 0); with ColorRec do begin IStructSize := SizeOf(TChooseColor); hWndOwner := HWindow; rgbResult := Color; IpCustColors := @CustColors; Flags := cc_FullOpen or cc_RGBInit; end; { Выполняем диалог вызовом функции ChooseColor} if ChooseColor(ColorRec) then begin Color:=ColorRec.rgbResult; { в Color заносится выбранный цвет } {Другие действия } end; end; Рассмотрим теперь выбор шрифта. Диалог выбора шрифта можно организовать с помощью функции ChooseFont(var CF: TChooseFont): Bool; где CF - запись типа TChooseFont, в которую заносится исходная информация о шрифте и в которой возвращается выбор пользователя. Функция ChooseFont создает и исполняет диалог, в котором поль- зователь может выбрать следующие атрибуты шрифта: название, стиль (например, утолщенный, курсив), размер, цвет и так называе- мые эффекты, т.е. является ли шрифт перечеркнутым или подчеркну- тым. ChooseFont возвращает True, если создание и выполнение диало- га прошло без ошибок и пользователь завершил выбор шрифта по кнопке Ок. В противном случае (возникла ошибка или диалог был
Гпава 5. Ресурсы Windows 249 завершен по кнопке Cancel либо по команде Close системного меню) возвращается False. Запись TChooseFont объявлена следующим образом: TChooseFont = record IStructSize: Longint; hWndOwner: HWnd; hDC: HDC; IpLogFont: PLogFont; rPointSize: Integer; Flags: Longint; rgbColors: Longint; ICustData: Longint; IpfnHook: function (Wnd: HWnd; Msg, wParam: Word; IParam: Longint): Word; IpTemplateName: PChar; hlnstance: THandle; IpszStyle: PChar; nFontType: Word; nSizdMin: Integer; nSizeMax: Integer; end; Поясним назначение полей записи: • IStructSize - размер записи TChooseFont в байтах. Это поле за- полняется перед вызовом функции ChooseFont и должно быть равно SizeOf(TChooseFont); • hWndOwner - дескриптор окна, назначаемого владельцем диало- гового блока выбора шрифта. Если поле hWndOwner равно 0, то диа- логовый блок не будет иметь владельца; • hDC - контекст отображения принтера, для которого предусмат- ривается выбор шрифта. Значение поля hDC нужно устанавливать перед вызовом функции ChooseFont только в том случае, если в поле Flags указан флаг cf_PrinterFonts; • IpLogFont - указатель на запись типа TLogFont. Запись TLogFont содержит параметры логического шрифта (высоту, ширину, угол на- клона выводимого текста к оси X, степень утолщенности, стиль, эф- фекты, семейство шрифта, другие параметры). Запись, адресуемая указателем IpLogFont, используется как для передачи в функцию ChooseFont начальных параметров логического шрифта, так и для возврата параметров, указанных пользователем в окн§ диалога; • iPointSize - размер шрифта в десятых долях точки. Это поле ус- танавливается функцией ChooseFont при выборе пользователем в диалоге кнопки Ок; • Flags - флаги, определяющие способ создания и работы диалого- вого окна выбора шрифта. Значением поля Flags может быть комби- нация констант из табл. 5.13;
250 Программирование в среде Borland Pascal для Windows Табл. 5.13. Флаги функции ChooseFont Константа Описание cf_ScreenFants Означает, что надо показывать экранные шриф- ты, инсталлированные в системе cf_PrinterFonts Означает, что надо показывать принтерные шрифты, доступные в контексте устройства hDC cf_Both cf_ShowHelp Равно cf_ScreenFonts or cf_PrinterFonts Помещает в диалоговое окно кнопку Help. При выборе пользователем этой кнопки окну с деск- риптором hWndOwner посылается соответствую- щее уведомительное сообщение cf_EnableHook Разрешает использование функции-перехватчика IpfnHook для обработки сообщений, предназна- ченных окну диалога cf_EnableTemplate Разрешает использование шаблона с именем IpTemplateName и дескриптором hlnstance для создания диалогового блока cf_EnableTemplateHandle Разрешает использование шаблона с дескрипто- ром hlnstance для создания диалогового блока. Значение поля IpTemplateName при этом игнори- руется cfJ nitT oLogfont Str uct Указывает, что запись, адресуемая полем IpLogFont, должна использоваться для установки начальных параметров шрифта cf_UseStyle Означает, что выбранный в комбинированном блоке Font Style стиль шрифта (Regular, Italic, Bold, Bold Italic) кодируется строкой IpszStyle. Если флаг не указан, то значение стиля кодируют поля записи IpLogFont cfJEffects Указывает, что в диалоговом окне можно задать для шрифта эффекты и цвет. Эффекты регулиру- ются полями IfStrikeOut и IfUnderline записи IpLogFont. Цвет хранится в поле rgbColors cf_Apply cf_AnsiOnly Помещает в диалоговое окно кнопку Apply Означает, что показывать надо только шрифты, в которых используется ANSI-кодирование cf_N о VectorF onts, cf_NoOEMFonts cf_NoSimulations Исключает из множества выбираемых шрифтов векторные шрифты Запрещает моделирование стилей шрифта функ- циями GDI, если в самом шрифте они не преду- смотрены cfJLimitSize Указывает, что размер выбираемого шрифта ограничен значениями полей nSizeMin и nSizeMax
Гпава 5. Ресурсы Windows 251 Окончание табл. 5.13 Константа Описание cf_FixedPitchOnly Допускает выбор только непропорционального шрифта cf_ WYSIWYG Используется совместно с флагами cf_Both, cf_ScalableOnly и означает, что показывать нужно только те шрифты, которые одновременно дос- тупны и на экране, и на принтере cf_ForceFontExist Означает, что при попытке выбора в диалоговом окне неправильного шрифта или стиля должно генерироваться сообщение об ошибке cf_ScalableOnly Допускает выбор только масштабируемого шрифта cf_TTOnly cf_NoFaceSel Допускает выбор только шрифта True-Type Указывает, что в комбинированном блоке с меткой Font строка выбранного шрифта является пустой cf_NoStyleSel Указывает, что в комбинированном блоке Font Style строка выбранного стиля является пустой cf_NoSizeSel Указывает, что в комбинированном блоке Size строка выбранного размера является пустой • rgbColors - цвет шрифта в формате RGB. В этом поле передается начальный цвет шрифта и возвращается результирующий. Поле rgbColors используется только тогда, когда указан флаг cf_Effects; • ICustData - служит для передачи в функцию IpfnHook произ- вольных данных пользователя при создании окна диалога; • IpfnHook - функция, которая перехватывает сообщения, предна- значенные окну диалога. Возможность использования этой функции обеспечивается установкой флага cf_EnableHook; • IpTemplateName - имя пользовательского файла ресурсов, в ко- тором содержится шаблон диалога выбора шрифта; • hlnstance - дескриптор шаблона диалога выбора шрифта. Это поле используется только в том случае, когда в поле Flags установлен флаг cf_EnableTemplateHandle или cf_EnableTemplate; • IpszStyle - нуль-терминированная строка, содержащая стиль шрифта. Используется только тогда, когда в поле Flags установлен флаг cf_UseStyle. Заполняется как перед вызовом, так и в результате работы функции ChooseFont; • nFontType - тип шрифта (см. табл. 5.14); • nSizeMin - задает минимально допустимый размер выбираемого шрифта; используется вместе с флагом cf_LimitSize;
252 Программирование в среде Borland Pascal для Windows • nSizeMax - задает максимально допустимый размер выбираемо- го шрифта; используется вместе с флагом cf_LimitSize. Табл. 5.14. Типы шрифта Тип шрифта Описание Simulated_FontType Шрифт моделируется функциями GDI Printer_FontType Принтерный шрифт Screen_FontType Экранный шрифт Bold_FontType Утолщенный шрифт ItalicJFontType Курсив . Regular_F ontT ype Регулярный шрифт Для того чтобы организовать выбор шрифта по команде меню с кодом cm_Font, необходимо в пользовательском оконном объекте объявить два поля и метод реакции на эту команду: TMainWindow = object(TWindow) LogFont: TLogFont; { параметры логического шрифта } FontColor: TColorRef; { цвет шрифта в формате RGB } procedure CMFont(var Msg: TMessage) virtual cm_First + cm_Font; end; Вот один из вариантов метода CMFont: procedure TMainWindow.CMFont(var Msg: TMessage); var FontRec: TChooseFont; begin { Заполняем запись FontRec начальными значениями } FillChar(FontRec, SizeOf(FontRec), 0); with FontRec do begin IStructSize := SizeOf(FontRec); hWndOwner := HWindow; IpLogFont := ©LogFont; Flags := cf_ScreenFonts or cf_Effects or cfJnitToLogFontStruct; rgbColors := FontColor; end; { Выполняем диалог вызовом функции ChooseFont} if ChooseFont(FontRec) then begin FontColor := FontRec.rgbColors; {Другие действия } end; end;
Гпава 5. Ресурсы Windows 253 После того как пользователь выбрал шрифт, из временной пере- менной FontRec копируется лишь значение цвета (rgbColors). Пара- метры логического шрифта возвращаются в записи LogFont. В заключение обзора функций ChooseColor и ChooseFont приведем программу COMMDIAL, которая отображает в окне текст Borland Pascal. Вывод текста осуществляется в методе Paint оконного объекта. Выбор фонового цвета для этого текста выполняет функция ChooseColor, а выбор шрифта и цвета букв - функция ChooseFont: COMMDIAL.INC const idm_Menu = 100; cm_Color =101; cm_Font = 102; COMMDIAL.PAS program CommDial; {$R COMMDIAL.RES} uses WinTypes, WinProcs, Strings, OWindows, CommDIg; {$1 COMMDIAL.INC} type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) BkColor: TColorRef; { фоновый цвет выводимого текста } CustColors: array [0..15] of TColorRef; {дополнительные цвета} LogFont: TLogFont; {параметры шрифта для выводимого текста } FontColor: TColorRef; { цвет текста } constructor lnit(AParent: PWindowsObject; AName: PChar); procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; procedure CMColor(var Msg: TMessage); virtual cm_First + cm_Color; procedure CMFont(var Msg: TMessage); virtual cm_First + cm_Font; end; PCommDialApp = ATCommDialApp; TCommDialApp = object(TApplication) procedure InitMainWindow; virtual; end; { Конструирует оконный объект и устанавливает параметры шрифта } constructor TMainWindow.lnit(AParent: PWindowsObject; AName: PChar); var I: Integer; begin inherited lnit(AParent, AName); Attr.Menu := LoadMenu(Hlnstance, PChar(idm_Menu)); „ BkColor := RGB(255, 255, 255); for I := 0 to 15 do CustColors[l] := 0; with LogFont do begin IfHeight := 50; IfWidth := 0; IfEscapement := 0; IfOrientation := 0;
254 Программирование в среде Borland Pascal для Windows IfWeight := fw_Bold; Hltalic := 0; IfUnderline := 0; IfStrikeOut := 0; IfCharSet := ANSI_CharSet; IfOutPrecision := Out_Default_Precis; IfClipPrecision := Clip_Default_Precis; IfQuality := Default-Quality; IfPitchAndFamily := Variable_Pitch; StrCopy(lfFaceName, Times New Roman’); end; FontColor := 0; end; { Выводит строку Worland Pascal' заданным шрифтом и цветом } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var OldFont, NewFont: HFont; R: TRect; begin NewFont := CreateFontlndirect(LogFont); OldFont := SelectObject(PaintDC, NewFont); SetBkColor(PaintDC, BkColor); SetTextColor(PaintDC, FontColor); GetClientRect(HWindow, R); DrawText(PaintDC, 'Borland Pascal', -1, R, dt-SingleLine or dt_Center or dt_VCenter); SelectObject(PaintDC, OldFont); end; { Организует диалог для выбора фонового цвета } procedure TMainWindow.CMColor(var Msg: TMessage); var ColorRec: TChooseColor; begin FillChar(ColorRec, SizeOf(ColorRec), 0); with ColorRec do begin IStructSize := SizeOf(TChooseColor); hWndOwner := HWindow; rgbResult := BkColor; IpCustColors := @CustColors; Flags := cc_FullOpen or cc_RGBInit; end; if ChooseColor(ColorRec) then begin BkColor := ColorRec.rgbResult; lnvalidateRect(HWindow, nil, True); end; end; { Организует диалог для выбора параметров шрифта } procedure TMainWindow.CMFont(var Msg: TMessage); var FontRec: TChooseFont;
Гпава 5. Ресурсы Windows 255 begin FillChar(FontRec, SizeOf(FontRec), 0); with FontRec do begin IStructSize := SizeOf(FontRec); hWndOwner := HWindow; IpLogFont := @LogFont; Flags := cf_ScreenFonts or cf_Effects or cfJnitToLogFontStruct; rgbColors := FontColor; end; if ChooseFont(FontRec) then begin FontColor := FontRec.rgbColors; lnvalidateRect(HWindow, nil, True); end; end; { Создает объект главного окна программы } procedure TCommDialApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, 'Common Dialogs’)); end; var CommDialApp: TCommDialApp; begin CommDialApp. Init(’CommDiar); CommDialApp.Run; CommDialApp.Done; end. Тема диалоговых блоков тесно связана с работой управляющих элементов. Однако их обсуждение выходит за рамки ресурсов Windows, так как управляющие элементы можно создавать динамиче- ски (а не только загружать из ресурса) и помещать в любое окно (не только диалоговое). Управляющим элементам посвящена гл. 6. В ней подробно рассматривается механизм передачи данных, который хотя и используется чаще всего в окнах диалога, но требует более полного описания устройства управляющих объектов OWL и методов работы с ними. 5.11. ИНСТРУМЕНТАЛЬНАЯ ПАНЕЛЬ 5.11.1. ОБЩИЕ СВЕДЕНИЯ Кроме стандартных ресурсов, система Windows позволяет применять и пользовательские ресурсы, их формат и порядок работы определяет сам программист. Данная возможность была учтена разработчиками OWL при создании ресурса инструментальной панели.
256 Программирование в среде Borland Pascal для Windows Инструментальная панель (toolbar) состоит из кнопок, которые обычно располагаются рядом с меню и служат ускорителями для его команд. Каждая кнопка содержит рисунок, показывающий, для чего она предназначена. При нажатии кнопок инструментальной панели генерируются командные сообщения, которые в программе должны обрабатываться точно так же, как и команды меню. Поскольку эти