/
Текст
Д. А. Сурков К.А. Сурков 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) состоит из кнопок, которые обычно располагаются рядом с меню и служат ускорителями для его команд. Каждая кнопка содержит рисунок, показывающий, для чего она предназначена. При нажатии кнопок инструментальной панели генерируются командные сообщения, которые в программе должны обрабатываться точно так же, как и команды меню. Поскольку эти команды дублируют команды меню, в программе требуется только создать инструментальную панель и присоединить ее к главному окну; обработку команд инструментальной панели выполняют те же мето- ды ответа на сообщения, что и в случае команд меню. В составе BPW имеется модуль TOOLBAR, который позволяет без особых хлопот подключить в программе инструментальную панель. Он расположен по маршруту EXAMPLES\WIN\TOOLBAR, считая от каталога, в котором инсталлирован Borland Pascal. В модуле TOOLBAR существует объект TToolBar, который реали- зует работу инструментальной панели. Объект TToolBar требует, что- бы инструментальная панель была оформлена в виде ресурса. Поэто- му прежде, чем мы перейдем к обсуждению объекта TToolBar, рас- смотрим, что представляет собой инструментальная панель в ресурсе. 5.11.2. РАЗРАБОТКА РЕСУРСА ИНСТРУМЕНТАЛЬНОЙ ПАНЕЛИ В Windows не существует стандартного типа ресурсов для инструмен- тальной панели. Поэтому мы должны сначала создать новый тип ре- сурса, а затем уже непосредственно сам ресурс этого типа. Инструмен- тальная панель должна иметь тип ресурса TOOLBARDATA. Чтобы определить новый тип ресурса, в Resource Workshop созда- ем или открываем уже существующий файл ресурсов и выбираем в меню команду Resource-New. В появившемся диалоговом окне нажи- маем кнопку New Туре и вводим новый тип ресурса TOOLBARDATA. На вопрос, следует ли создавать новый идентификатор, отвечаем No (Нет). В результате Resource Workshop создает новый тип ресурса и помещает его имя в список доступных типов ресурсов. Теперь выбираем в списке доступных типов ресурсов строку TOOLBARDATA и нажимаем кнопку Ok. Resource Workshop поме- щает в проект новый ресурс типа TOOLBARDATA и открывает окно редактора, в котором появятся следующие строки: TOOLBARDATA_1 TOOLBARDATA BEGIN END TOOLBARDATA_ 1 - это идентификатор инструментальной пане- ли, который Resource Workshop присваивает ресурсу по умолчанию. Его можно заменить на более удобный целочисленный идентифика- тор idt_ToolBar. Чтобы определить идентификатор idt_ToolBar, нуж-
Гпава 5. Ресурсы Windows 257 но воспользоваться командой меню Resource-Identifiers.... Значение константы idt_ToolBar может быть произвольным: idt_ToolBar TOOLBARDATA BEGIN END Между словами BEGIN и END определяются данные инструмен- тальной панели. Эти данные представляют собой массив двухбайтных чисел, записанных через пробел или символ перевода строки. Как правило, вместо чисел используются именованные константы. Первое число - это количество элементов инструментальной панели. За ним следуют пары чисел, в которых первым является идентификатор рас- трового изображения кнопки в ресурсе, а вторым - код команды, ко- торую генерирует кнопка. Отметим, что под кнопкой инструменталь- ной панели понимается не управляющий элемент типа BUTTON, а небольшое растровое изображение, т.е. ресурс типа BITMAP. В следующем примере определены две кнопки: tb.About, генери- рующая команду cm_About (отображение диалогового окна About), и tbJExit, генерирующая команду cmJExit (выход из программы): idt_ToolBar TOOLBARDATA BEGIN 2 tb_About cm_About tb_Exit cm_Exit END Идентификаторы tb.About и tb_Exit следует определить заранее, причем обязательно сделать их числовыми. С такими же идентифика- торами нужно создать в файле ресурса два растровых изображения для кнопок инструментальной панели. Размеры каждого изображения могут быть произвольными, так как ширина панели устанавливается автоматически в зависимости от величины кнопок. Мы будем исполь- зовать кнопки размером 20x20 пикселов. Каждое изображение удобно хранить на диске в виде ВМР-файла, чтобы его можно было добавить к любому проекту. Поэтому растро- вые изображения для кнопок с идентификаторами tbJAbout и tb_Exit мы поместили в файлы ABOUT.BMP и EXIT.BMP, а потом добавили в проект с помощью команды меню File-Add to project.... Кнопки инструментальной панели могут разделяться пустыми промежутками. Например, чтобы между кнопками tb.About и tb.Exit вставить пустой промежуток в 8 экранных точек, нужно определить инструментальную панель следующим образом: 9 Зак. 1049
258 Программирование в среде Borland Pascal для Windows idt.ToolBar TOOLBARDATA BEGIN 3 ; общее количество пар с учетом пустых промежутков tb_About cm_About О ; вместо идентификатора кнопки записывается О 8 ; вместо команды указывается ширина пустого промежутка tb_Exit cmExit END 5.11.3. ПОДКЛЮЧЕНИЕ ИНСТРУМЕНТАЛЬНОЙ ПАНЕЛИ Для того чтобы подключить в программе инструментальную панель, необходимо создать экземпляр объекта TToolBar, порожденного от TWindow. В его конструктор Init передаются три параметра: указа- тель на родительский оконный объект, идентификатор инструмен- тальной панели в ресурсе и ориентация инструментальной панели. Ориентация может быть следующей: • tbHorizontal - горизонтальная панель; • tbLeftVertical - вертикальная панель в левой части окна; • tbRightVertical - вертикальная панель в правой части окна. В приведенной ниже строке создается горизонтальная инструмен- тальная панель, которая в ресурсе имеет числовой идентификатор idt_ToolBar: Toolbar := New(PToolbar, lnit(@Self, PChar(idt_ToolBar), tbHorizontal)); Передача первым параметром конструктора Init значения @Self предполагает, что объект Toolbar создается в некотором родитель- ском оконном объекте. Этого однако недостаточно для того, чтобы инструментальная панель имела правильные размеры, так как кон- троль этих величин возлагается на родительское окно. Естественно, объект Toolbar в состоянии сам вычислить свои размеры, но инициа- тором этих действий должно быть окно родителя. Для того чтобы объект Toolbar заново пересчитал свои размеры и при необходимости перерисовал себя, ему нужно послать сообщение с кодом am_CalcParentClientRect. При этом в параметре WParam нужно передать 1, а в параметре LParam - адрес записи THna TRect, в кото- рой содержатся текущие координаты рабочей области окна. Если требуется только получить информацию о величине рабочей области без инструментальной панели, в параметре WParam необходимо пере- дать нуль. В объекте главного окна лучше всего объявить специальный метод для пересчета рабочей области окна. В простейшем случае он может выглядеть так:
Гпава 5. Ресурсы Windows 259 procedure TMainWindow.RedoClientRect; var R: TRect; begin GetClientRect(HWindow, R); SendMessage(ToolbarA.HWindow, am_CalcParentClientRect, 1, Longint(@R)); end; Посылка сообщения am_CalcParentClientRect объекту Toolbar должна выполняться родительским объектом в ответ на сообщение wm_Size. Чтобы самому не переписывать уже реализованную в объек- те TWindow реакцию на сообщение wm_Size, лучше переписать в на- следнике метод DefWndProc, который предназначен для обработки сообщений по умолчанию: procedure TMainWindow.DefWndProc(var Msg: TMessage); begin if Msg.Message = wm_Size then RedoClientRect else inherited DefWndProc(Msg); end; Следующая несложная программа демонстрирует работу инстру- ментальной панели: TOOLS.INC const idm_Menu = 100; cm_About = 999; idt_ToolBar = 101; tb_About = 101; tb_Exit = 102; idd_AboutBox = 100; idi_Aboutlcon = 100; TOOLS.PAS program Tools; {$R TOOLS.RES} uses WinTypes, WinProcs {$ifdefVer80}, Messages {$endif}, OWindows, ODialogs, Toolbar; {$1 TOOLS.INC} type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) Toolbar: PToolbar; { инструментальная панель } constructor lnit(AParent: PWindowsObject; ATitle: PChar); procedure RedoClientRect; procedure DefWndProc(var Msg: TMessage); virtual; procedure CMAbout(var Msg: TMessage); virtual cm_First + cm_About; end;
260 Программирование в среде Borland Pascal для Windows PToolsApp = ATToolsApp; TToolsApp = object(TApplication) prpcedure InitMainWindow; virtual; end; { Конструирует оконный объект, загружая меню и инструм. панель } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); Attr.Menu := LoadMenu(Hlnstance, PChar(idm_Menu)); Toolbar := New(PToolbar, lnit(@Self, PChar(idt_Toolbar), tbHorizontal)); end; { Пересчитывает рабочую область окна, вычитая из нее инструм. панель } procedure TMainWindow.RedoClientRect; var R: TRect; begin GetClientRect(HWindow, R); SendMessage(ToolbarA.HWindow, am_CalcParentClientRect, 1, Longint(@R)); end; { Перехватывает сообщение wm_Size для пересчета рабочей области окна } procedure TMainWindow.DefWndProc(var Msg: TMessage); begin if Msg.Message = wm_Size then RedoClientRect else inherited DefWndProc(Msg); end; { Открывает окно диалога About по команде из меню или инструм. панели } procedure TMainWindow.CMAbout(var Msg: TMessage); begin ApplicationA.ExecDialog(New(PDialog, lnit(@Self, PChar(idd_AboutBox)))); end; { Создает объект главного окна программы } procedure TToolsApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Toolbar Demo')); end; var ToolsApp: TToolsApp; begin ToolsApp. I nit(’Tools’); ToolsApp.Run; ToolsApp. Done; end. На этом мы заканчиваем рассмотрение ресурсов и переходим к изучению управляющих элементов Windows и ассоциированных с ними интерфейсных объектов OWL.
6 ЭЛЕМЕНТЫ УПРАВЛЕНИЯ В ОКНАХ 6.1. ОСНОВНЫЕ ЭЛЕМЕНТЫ УПРАВЛЕНИЯ Элементы управления в окнах (controls) - это хорошо известные каж- дому, кто работал с Windows, кнопки, блоки списков, блоки редакти- рования, полосы скроллинга и т.д. (рис. 6.1). Они предназначены для организации наиболее удобной формы диалога между программой и пользователем. Цель программиста - правильно выбрать необходи- мые для решения конкретной задачи элементы управления, разместить их в окне и обеспечить согласованную работу. Dialog Button ListBox item 1 ListBox item 2 □ CheckBox "GroupBox ® RadioButton 1 О RadioButton 2 ListBox item 3 ListBox item 4 ComboBox Static Edit Рис. 6.1. Окно с различными элементами управления Управляющие элементы являются специализированными дочерни- ми окнами других окон. В диалоговых окнах они могут загружаться из ресурса или создаваться динамически. Для окон любого другого типа возможен только второй способ. Работа с управляющими элементами осуществляется через управ- ляющие объекты OWL, которые мы и рассмотрим ниже. Все типы управляющих объектов определены в модуле ODIALOGS и являются наследниками базового управляющего объекта TControl (рис. 6.2).
262 Программирование в среде Borland Pascal для Windows Рис. 6.2. Иерархия управляющих объектов Объект TControl - родоначальник всех управляющих объектов. Он порожден от TWindow и имеет следующие особенности: • не регистрирует оконного класса, так как оконные классы управ- ляющих элементов уже известны системе Windows; • запрещает рисование управляющих элементов из прикладной программы, так как это делает Windows. Основной набор управляю- щих элементов (Windows-style) рисуется функциями модуля USER.EXE. Дополнительные управляющие элементы (Borland-style) рисуются средствами библиотеки BWCC.DLL. Тип TControl является абстрактным, т.е. его экземпляры никогда не создаются. В программе используются его наследники: • TButton - нажимающаяся кнопка; • TCheckBox - кнопка с независимой фиксацией; • TRadioButton - кнопка с зависимой фиксацией; • TGroupBox - группа элементов управления; • TStatic - статический (нередактируемый) текст; • TEdit - редактируемый текст; • TListBox - блок списка; • TComboBox - комбинированный блок (строка редактора плюс блок списка); • TScrollBar - горизонтальная (вертикальная) полоса скроллинга. Каждому из перечисленных объектов соответствует управляющий элемент Windows, создаваемый на основе предопределенного оконно- го класса. Имена этих классов зарезервированы: Button, Static, Edit, ListBox, ComboBox, ScrollBar. Разработка новых элементов управле-
Гпава 6. Элементы управления в окнах 263 ния предполагает регистрацию новых оконных классов с уникальны- ми именами. Например, программисты фирмы Borland International разработали свои оконные классы управляющих элементов с именами BorBtn, BorCheck, BorRadio, BorStatic, BorShade и поместили их в библиотеку BWCC. Для работы с ними используются те же управ- ляющие объекты OWL. 6.2. СОЗДАНИЕ ЭЛЕМЕНТОВ УПРАВЛЕНИЯ Чтобы создать элемент управления, нужно сформировать управляю- щий объект соответствующего типа. Управляющий объект имеет два конструктора: Init и InitResource. Первый конструктор применяется, если управляющий элемент соз- дается динамически; второй, - если элемент загружается из ресурса вместе с окном диалога. Обратите внимание на то, что конструктор создает только объект, но не управляющий элемент, поэтому вызов Init (InitResource) не приводит к немедленному созданию (загрузке) управляющего элемента, а лишь обеспечивает это в дальнейшем при создании его родительского окна. Оба конструктора имеют два обя- зательных параметра: указатель на родительский объект и идентифи- катор управляющего элемента. Идентификатор управляющего элемента - это целое положитель- ное число, используемое для работы с данным элементом в програм- ме. Он помещается в поле Attr.Id и в отличие от дескриптора HWindow задается программистом. В пределах родительского окна идентификатор управляющего элемента должен быть уникальным. Его принято оформлять как именованную константу языка Borland Pascal, начинающуюся с префикса id_ (например, id_Ok, id_Cancel). Следующий фрагмент программы показывает, как в окне диалога создать блок редактора и нажимающуюся кнопку (предполагается, что окно диалога загружается из ресурса, блок редактора имеет в диа- логе идентификатор 101, а кнопка - 102): const id_Edit =101; id_Button = 102; type TMyDialog = object(TDialog) f’ Edit: PEdit; Button: PButton; constructor lnit(AParent: PWindowsObject; AName: PChar); end; constructor TMyDialog.lnit(AParent: PWindowsObject; AName: PChar); begin inherited lnit(AParent, AName);
264 Программирование в среде Borland Pascal для Windows Edit := New(PEdit, lnitResource(@Self, id_Edit, 20); Button := New(PButton, lnitResource(@Self, id_Button); end; Создаваемые управляющие объекты автоматически добавляют се- бя в список дочерних объектов родителя, поэтому результат работы конструктора TMyDialog.Init представляет собой дерево объектов, состоящее из одного родительского и двух дочерних управляющих. При запуске экземпляра TMyDialog на исполнение из ресурса про- граммы будет загружено окно диалога с его управляющими элемен- тами. Объект Edit свяжется с управляющим элементом, имеющим идентификатор 101, a Button - с элементом, имеющим идентификатор 102. Следует помнить, что элемент управления не может быть загружен из ресурса отдельно от родительского окна диалога. 6.3. ОБРАБОТКА СООБЩЕНИЙ ОТ ЭЛЕМЕНТОВ УПРАВЛЕНИЯ Взаимодействие окна с элементами управления основано на механиз- ме сообщений. Сообщения управляющих элементов разделяют на два типа: управляющие и уведомительные. Управляющие сообщения служат для воздействия на элементы управления из программы. Среди этих сообщений можно выделить следующие группы: • сообщения, посылаемые всем управляющим элементам (их иден- тификаторы начинаются с префикса wm_); • сообщения, посылаемые кнопкам (bm_, dm_); • сообщения, посылаемые редактору (ет_); • сообщения, посылаемые блоку списка (1Ь_); • сообщения, посылаемые комбинированному блоку (cb_). В программе не следует посылать эти сообщения напрямую, лучше использовать методы ассоциированного с управляющим элементом объекта. Уведомительные сообщения генерирует сам управляющий элемент, когда изменяет свое состояние (например, получает фокус ввода). Выделяют следующие группы уведомительных сообщений: • сообщения от кнопок (Ьп_); • сообщения от редактора (еп_); • сообщения от блока списка (lbn_); • сообщения от комбинированного блока (cbn_). Программа может реагировать на уведомительные сообщения од- них элементов, изменяя состояние других и обеспечивая их согласо- ванную работу.
Гпава 6. Элементы управления в окнах 265 Перехват уведомительных сообщений может выполняться либо в управляющем объекте, либо в его родительском объекте. В первом случае в управляющем объекте объявляется метод ответа на сообще- ние, селектор диапазона которого равен nf_First, а смещение внутри диапазона совпадает с кодом сообщения. Так, например, в объекте TCheckBox объявлен метод реакции на сообщение bn_Clicked, посы- лаемое при нажатии пользователем кнопки с независимой фиксацией: TCheckBox = object(TButton) procedure BNCIicked(var Msg: TMessage); virtual nf_First + bn_Clicked; end; Если в управляющем объекте отсутствует метод ответа на уведо- мительное сообщение или управляющий элемент вообще не имеет ассоциированного с ним объекта, то сообщение посылается родитель- скому оконному объекту. В родительском объекте метод ответа на уведомительное сообщение должен иметь селектор диапазона id_First, а смещение внутри диапазона должно совпадать с идентификатором управляющего элемента. Примером служит определение в объекте TDialog методов реакции на нажатие кнопок Ок и Cancel: TDialog = object(TWindowsObject) procedure Ok(var Msg: TMessage); virtual id_First + id_Ok; procedure Cancel(var Msg: TMessage); virtual id_First + id_Cancel; end; Рассмотрим теперь существующие в OWL типы управляющих объ- ектов и особенности работы с ними. 6.4. ТИПЫ УПРАВЛЯЮЩИХ ОБЪЕКТОВ 6.4.1. ОБЪЕКТ TButton Объект TButton ассоциируется с нажимающейся кнопкой (push button). По умолчанию оконный класс объекта TButton называется Button, однако если в программе подключается библиотека управляющих элементов BWCC, то он называется BorBtn. Конструктор Init объекта TButton имеет следующий формат: TButton.Init(AParent: PWindowsObject; Anld: Integer: AText: PChar; X, Y, W, H: Integer; IsDefault: Boolean); где AParent - адрес родительского оконного объекта; Anld - иденти-
266 Программирование в среде Borland Pascal для Windows фикатор кнопки в родительском окне; AText - надпись на кнопке; X,Y - координаты левого верхнего угла кнопки; W,H - ширина и высота кнопки; IsDefault - определяет, будет ли кнопка срабатывать по умолчанию (кнопка, срабатывающая по умолчанию, выделяется утолщенной черной рамкой и отличается от обычной тем, что выби- рается нажатием клавиши Enter, если только с помощью мыши или клавиатуры не выбрана другая кнопка или блок). Если кнопка располагается в загружаемом из ресурса окне диало- га, то управляющий объект TButton создается конструктором: TButton.InitResource(AParent: PWindowsObject; ResourcelD: Word); где A Parent - адрес родительского оконного объекта; ResourcelD - идентификатор кнопки в родительском окне. Поведение кнопки задается с помощью специальных оконных сти- лей, устанавливаемых в поле Attr.Style при создании объекта TButton. В табл. 6.1 приведены все существующие стили кнопок, включая стили независимых и зависимых кнопок. Нажимающаяся кнопка использует лишь два из них: bs_PushButton и bs_DefPushButton. Сообщения от всех типов кнопок удобнее всего обрабатывать в родительском окне. 6.4.2. ОБЪЕКТ TCheckBox Объект TCheckBox является наследником TButton и ассоциируется с кнопкой независимой фиксации. Независимые кнопки типа TCheckBox создаются на основе стандартного оконного класса Button, однако если в программе подключен модуль BWCC, - то на основе класса BorCheck. Конструктор Init объекта TCheckBox имеет следующий формат: TCheckBox.Init(AParent: PWindowsObject; Anld: Integer; AText: PChar; X, Y, W, H: Integer; AGroup: PGroupBox); где A Parent - адрес родительского оконного объекта; Anld - иденти- фикатор кнопки в родительском окне; A Text - надпись рядом с кноп- кой; X, Y - координаты левого верхнего угла кнопки; W,H - ширина и высота кнопки; AGroup - блок группы, в котором находится кнопка (если кнопка располагается отдельно, в этом параметре следует пере- давать значение nil). Конструктор InitResource объекта. TCheckBox имеет такой же фор- мат, как TButton.InitResource, поэтому здесь он опущен. При исполь- зовании конструктора InitResource все атрибуты кнопки берутся из ресурса. Внешний вид и особенности поведения независимой кнопки опре- деляются специальными оконными стилями (табл. 6.1), устанавливае-
Гпава 6. Элементы управления в окнах 267 мыми в поле Attr.Style. Для независимой кнопки допустимы следую- щие стили: bs_LeftText, bs_CheckBox, bs_AutoCheckBox, bs_3State, bs_Auto3State. Табл, 6,1, Стили кнопок Константа Описание bs_LeftText Размещает поясняющий текст слева от зависимой или независимой кнопки bs_PushButton bs_DefPush Button Нажимающаяся кнопка с надписью Нажимающаяся кнопка, с которой связано действие по умолчанию. В диалоговом окне только одна кнопка должна иметь стиль bs_DefPushButton bs_CheckBox Кнопка независимой фиксации, которая может нахо- диться в двух состояниях: включена или выключена bs_AutoCheckBox Кнопка независимой фиксации, состояние которой отслеживается автоматически без участия программи- ста bs_3State Кнопка независимой фиксации, которая может нахо- диться в одном из трех состояний: включена, выключе- на или это неизвестно. В неопределенном состоянии кнопка затемняется серым цветом bs_Auto3State То же, что и bs_3State, но смена состояний выполняется автоматически по действиям пользователя bs_RadioButton Кнопка зависимой фиксации. В группе зависимых кно- пок только одна может быть включена bs_AutoRadioButton То же, что и bs_RadioButton, но переключение кнопки происходит автоматически: когда одна кнопка в группе включается, все остальные выключаются bs_GroupBox Рамка группы с поясняющим текстом. Предназначена для внешнего выделения связанных по смыслу элемен- тов управления bs_OwnerDraw Кнопка, рисуемая владельцем. Сообщения о действиях пользователя над этим элементом управления переда- ются родительскому окну, которое должно его отобра- жать, инвертировать и т.п. Стиль bs_OwnerDraw не может комбинироваться ни с каким другим стилем bs_UserButton То же, что и bs_OwnerDraw, но при выборе пользова- телем элемента управления его область автоматически выделяется пунктирной рамкой Обычно кнопка независимой фиксации имеет два состояния: вклю- чена или выключена. Однако если при конструировании экземпляра TCheckBox установить стиль bs_Auto3State или bs_3State, то кнопка будет иметь три состояния. Третье состояние - неопределенное. Кноп-
268 Программирование в среде Borland Pascal для Windows ка, создаваемая со стилем bs_AutoCheckBox или bs_Auto3State, стано- вится автоматической. Это значит, что при нажатии она сама пере- ключается из состояния в состояние без вмешательства программиста. По умолчанию конструктор Init создает автоматическую кнопку не- зависимой фиксаций с двумя состояниями (bs_AutoCheckBox). Для управления независимой кнопкой из программы в объекте TCheckBox существуют следующие методы: Check - включает кнопку; Uncheck - выключает кнопку; Toggle - переключает кнопку между состояниями (если их три, то между тремя состояниями); SetCheck(CheckFlag: Word) - устанавливает кнопку в любое из трех состояний в зависимости от значения параметра CheckFlag. Значения CheckFlag могут быть следующими: bf_Unchecked - выключить; bf_Checked - включить; bf_Grayed - установить в 'неопределенное состояние; GetCheck: Word - возвращает текущее состояние кнопки. Значения функции соответствуют значениям параметра CheckFlag в методе SetCheck. 614.3. ОБЪЕКТ TRadioButton Объект TRadioButton является производным от TCheckBox и управ- ляет кнопкой зависимой фиксации, которую иначе называют селектив- ной кнопкой. Зависимые кнопки обычно объединяются в группы. При включении одной кнопки в группе все остальные выключаются. Оконный класс селективной кнопки называется Button (по умолча- нию) или BorRadio (если подключена библиотека BWCC). Конструктор Init объекта TRadioButton совпадает по формату с TCheckBox.Init, а конструктор InitResource заимствуется у TCheckBox (см. 6.4.2). Селективная кнопка может нормально работать только в группе с другими селективными кнопками, поэтому среди параметров конструктора Init передается адрес объекта управления группой. По умолчанию селективная кнопка создается как автоматическая. Для этого конструктор Init устанавливает в поле Attr.Style флаг bs_AutoRadioButton. Чтобы сделать кнопку неавтоматической, нужно при конструировании объекта вместо bs_AutoRadioButton установить стиль bs_RadioButton (см. табл. 6.1). При использовании конструкто- ра InitResource все атрибуты кнопки, в том числе и стиль, берутся из ресурса. Для управления селективной кнопкой используются методы TRadioButton, наследуемые из TCheckBox, с учетом небольшого ог- раничения: кнопка зависимой фиксации может иметь всего два со- стояния (включена или выключена).
Гпава 6. Элементы управления в окнах 269 Чтобы реагировать на переключения селективных кнопок, нужно для каждой из них определить метод ответа на сообщения. Это дела- ется в родительском объекте, например: TMyDialog = object(TDialog) procedure IDMyRadioButton1(var Msg: TMessage); virtual id_First + id_MyRadioButton1; procedure IDMyRadioButton2(var Msg: TMessage); virtual id_First + id_MyRadioButton2; procedure IDMyRadioButton3(var Msg: TMessage); virtual id_First + id_MyRadioButton3; end; 6.4.4. ОБЪЕКТ TGroupBox TGroupBox - это интерфейсный объект блока группы. Блок группы принадлежит к оконному классу Button. Объекты типа TGroupBox не используются как самостоятельные управляющие элементы, их основное назначение состоит в том, чтобы группировать кнопки (чаще всего селективные). Конструктор Init объекта TGroupBox имеет следующий формат: TGroupBox.Init(AParent: PWindowsObject; Anld: Integer; AText: PChar; X, Y, W,H: Integer); где AParent - адрес родительского оконного объекта; 'Anld - иденти- фикатор блока группы в родительском окне; A Text - заголовок блока группы; X, Y - координаты левого верхнего угла блока группы; Ж, Н- ширина и высота блока группы. Формат конструктора InitResource следующий: TGroupBox.InitResource(AParent: PWindowsObject; ResourcelD: Word); где AParent - адрес родительского оконного объекта; ResourcelD - идентификатор блока группы в родительском окне. Конструктор Init устанавливает в атрибутах управляющего эле- мента стиль bs_GroupBox (см. табл. 6.1) и выключает стиль ws_TabStop. Стиль bs_GroupBox делает управляющий элемент бло- ком группы, а стиль ws_TabStop определяет, будет ли на управляю- щем элементе задерживаться фокус ввода. Блок группы не предназна- чен для ввода данных, поэтому стиль ws_TabStop в нём выключен. Если для группы зависимых кнопок создать объект TGroupBox, можно избежать определения в родительском объекте большого ко- личества методов реакции на каждую кнопку, заменив их на один, инициируемый объектом TGroupBox. Этот динамический метод дол- жен иметь селектор диапазона id_First и смещение в диапазоне, равное идентификатору блока группы, например:
270 Программирование в среде Bprland Pascal для Windows TMyDialog = object(TDialog) MyGroupBox: PGroupBox; MyRadioButtonl: PRadioButton; MyRadioButtonl: PRadioButton; MyRadioButton3: PRadioButton; procedure IDMyGroupBox(var Msg: TMessage); virtual id_First + id_MyGroupBox; end; Если объекты MyRadioButtonl, MyRadioButtonl, MyRadioButton3 связать при конструировании с группой MyGroupBox (установить в каждом указатель Group), то метод IDMyGroupBox будет вызываться при переключении любой из кнопок. Идентификатор кнопки переда- ется в Msg.LParamHi. Булевское поле Notify Parent в объекте TGroupBox показывает, надо ли информировать родительское окно об изменении состояния кнопок группы, и по умолчанию равно True. 6.4.5. ОБЪЕКТ TStatic TStatic является интерфейсным объектом статического текста (управляющего элемента класса Static). Чаще всего статический текст служит подсказкой для других управляющих элементов и поэтому не задерживает фокуса ввода. Для элементов статического текста, загру- жаемых из ресурса, объекты TStatic обычно не создаются. Конструктор Init объекта TStatic имеет формат TStatic.Init(AParent: PWindowsObject; Anld: Integer; AText: PChar; X, Y, W, H: Integer; ATextLen: Word); где AParent - адрес родительского оконного объекта; Anld - иденти- фикатор статического текста в родительском окне; A Text - статиче- ский текст; X, Y- координаты левого верхнего угла статического тек- ста; W,H - ширина и высота прямоугольника, в который вписывается статический текст; ATextLen - максимальная длина статического тек- ста. Формат конструктора InitResource следующий: TStatic.InitResource(AParent: PWindowsObject; ResourcelD: Word; ATextLen: Word); где AParent - адрес родительского оконного объекта; ResourcelD - идентификатор статического текста в родительском окне; ATextLen - максимальная длина статического текста. Особенности отображения статического текста регулируются спе- циальными стилями (табл. 6.2), устанавливаемыми в поле Attr.Style. Конструктор Init устанавливает в атрибутах объекта стиль ssJLeft (статический текст, прижатый к левому краю) и выключает стиль ws_TabStop (на статическом тексте фокус ввода не задерживается).
Глава 6. Элементы управления в окнах 271 Табл, 6.2. Стили статического текста Константа Описание ss_Left Текст, выровненный по левому краю. Если длина текста больше, чем ширина элемента управления, не уместившийся текст переносится на новую строку. Строки разрываются на границах слов и прижимают- ся к левой границе ss_Center Центрированный текст. Если длина текста больше, чем ширина элемента управления, не уместившийся текст переносится на новую строку. Строки разрыва- ются на границах слов и центрируются ss_Right Текст, выровненный по правому краю. Если длина текста больше, чем ширина элемента управления, не уместившийся текст переносится на новую строку. Строки разрываются на границах слов и прижимают- ся к правой границе ss_Icon Пиктограмма. Текст в элементе управления является идентификатором пиктограммы в ресурсе приложе- ния. Ширина и высота элемента управления в этом случае игнорируются. Пиктограмма сама устанавли- вает свои размеры ss_BlackRect Прямоугольник цвета рамки окна (color_WindowFrame); обычно он черный ss_GrayRect Прямоугольник цвета фона окна (color_Background); как правило, он серый ss_WhiteRect Прямоугольник цвета окна (color_Window); обычно он белый ss_BlackFrame Рамка цвета рамки окна (color_WindowFrame); обыч- но она черная ss_Gray Frame Рамка цвета фона окна (color_Background); обычно она серая ss_WhiteFrame ss_UserItem Рамка цвета окна (color_Window); обычно она белая Статический элемент управления, рисуемый про- граммистом ss_Simple Одна строка текста, выровненная по левому краю. Текст не переносится на новую строку и не разбива- ется по словам ° ss_LeftN о Word Wrap Текст, выровненный по левому краю, без возможно- сти переноса слов ss_No Prefix Отключает трансляцию символа & в подчеркивание следующего за ним символа. Такая трансляция вы- полняется по умолчанию. Подчеркнутый символ служит для быстрого перехода к элементу управления
272 Программирование в среде Borland Pascal для Windows Статический текст нельзя редактировать, но можно изменять про- граммно. Для этой цели в TStatic существует ряд методов: SetText(ATextString: PChar) - помещает в управляющий элемент текстовую строку ATextString; GetText(ATextString: PChar; MaxChars: Integer): Integer - копирует строку из управляющего элемента в буфер ATextString и возвращает ее длину (без учета нуль-терминатора). Параметр MaxChars задает максимальное число копируемых символов, включая нуль- терминатор. .Если объем текста в управляющем элементе больше MaxChars, то лишние символы отбрасываются; GetTextLen: Integer - возвращает длину текста и может использо- ваться перед вызовом GetText. Значение (GetTextLen + 1) соответству- ет объему памяти, который нужно зарезервировать для приема всей строки из управляющего элемента; Clear - удаляет текст из элемента управления. 6.4.6. ОБЪЕКТ TEdit Объект TEdit порожден от TStatic и представляет собой редактор текста. Существует две разновидности редактора: однострочный и многострочный. Однострочный редактор служит для ввода различных названий, имен и другого относительно короткого текста. Как правило, он не имеет полос скроллинга, но разрешает прокручивать текст по гори- зонтали клавишами перемещения курсора влево и вправо. Многострочный редактор позволяет работать с большими объе- мами текста (до 64 Кбайт) и часто имеет одну или две полосы скрол- линга (вертикальную и горизонтальную). И однострочный, и много- строчный редакторы поддерживаются одним и тем же оконным клас- сом с именем Edit. Как и любой управляющий объект, TEdit имеет два конструктора: Init и InitResource. Формат конструктора Init: TEdit.Init(AParent: PWindowsObject; Anld: Integer; AText: PChar; X, Y, W, H: Integer; ATextLen: Word; Multiline: Boolean); где AParent - адрес родительского оконного объекта; Anld - иденти- фикатор редактора текста в родительском окне; AText - исходный текст в редакторе; X, Y - координаты левого верхнего угла редактора текста; W,H- ширина и высота блока редактора; ATextLen - макси- мальная длина текста в редакторе; Multiline - определяет, будет ли редактор многострочным. Конструктор InitResource имеет формат TEdit.InitResource(AParent: PWindowsObject; ResourcelD: Word; ATextLen: Word);
Гпава 6. Элементы управления в окнах 273 где AParent - адрес родительского оконного объекта; ResourcelD - идентификатор редактора текста в родительском окне; ATextLen - максимальная длина текста в редакторе. Свойства редактора задаются с помощью специальных стилей Windows (табл. 6.3), устанавливаемых в поле Attr.Style при конструи- ровании объекта TEdit. Конструктор Init включает стиль es_Left и es_AutoHScroll. Если создается многострочный редактор (параметр Multiline равен True), то дополнительно включаются стили ws_VScroll, wsJHScroll, esJMultiLine и es_AutoVScroll. При использовании InitResource атрибуты управляющего элемента TEdit берутся из ре- сурса. Объект TEdit предоставляет широкие возможности по управлению блоком редактора из программы благодаря своим многочисленным методам. Вот наиболее полезные из них: ClearModify - убирает признак модификации текста; IsModified: Boolean - позволяет узнать, изменялся ли в редакторе текст; Insert(ATextString: PChar) - вставляет текст в текущую позицию курсора; GetNumLines: Integer - возвращает общее число строк в много- строчном редакторе; GetLineLength(LineNumber: Integer): Integer - возвращает длину строки с номером LineNumber; DeleteLine(LineNumber: Integer): Boolean - удаляет строку с номе- ром LineNumber; GetLine(ATextString: PChar; StrSize, LineNumber: Integer): Boolean - заносит в буфер ATextString указанное в StrSize число символов из строки с номером LineNumber. При возникновении ошибки возвра- щает False; GetLineFromPos(CharPos: Integer): Integer - возвращает номер строки по номеру символа в текстовом буфере. Символы отсчитыва- ются от нуля, а переводы строки занимают два символа; GetLineIndex(LineNumber: Integer): Integer - возвращает число сим- волов в текстовом буфере, которые предшествуют строке с номером LineNumber. Если строка с указанным номером не существует, воз- вращается общее число символов в буфере редактора; GetSelection(var StartPos, EndPos: Integer) - помещает в StartPos и EndPos начальную и конечную позиции помеченного в редакторе текста; SetSelection(StartPos, EndPos: Integer): Boolean - помечает текст в редакторе; GetSubText(ATextString: PChar; StartPos, EndPos: Integer) - копиру- ет в ATextString фрагмент текста, заданный начальной и конечной позициями в буфере;
274 Программирование в среде Borland Pascal для Windows Табл. 6.3. Стили текстового редактора Константа Описание es_Left es_Center es_Right Текст выравнивается по левой границе Текст центрируется (только в многострочном редакторе) Текст выравнивается по правой границе (только в много- строчном редакторе) esJMultiLine Многострочный редактор текста (по умолчанию редактор создается как однострочный). Если многострочный редак- тор имеет полосы скроллинга, сообщения от них обрабаты- ваются автоматически es_UpperCase es_LowerCase es_Password Символы преобразуются к верхнему регистру Символы преобразуются к нижнему регистру Указывает, что в редакторе вводится пароль или другой секретный текст. Все вводимые символы отображаются в виде звездочки (*). Вместо звездочки может отображаться другой символ. Для этого органу управления нужно послать сообщение em_SetPasswordChar es_AutoVScroll Разрешает вертикальный скроллинг текста в многостроч- ном редакторе es_AutoHScroll Разрешает горизонтальный скроллинг текста в одностроч- ном и многострочном редакторах es_NoHideSel Подсветка выделенного блока сохраняется при потере ре- дактором фокуса ввода es_OEMConvert Перед отображением на экране выполняется двойное пре- образование текста: из таблицы ANSI в таблицу OEM и обратно. Это позволяет исключить ошибки при переводе полученных у пользователя символов в таблицу OEM. Стиль es_OEMConvert в основном используется для ввода имени файла es_ReadOnly Не допускает изменение текста в редакторе. Текст можно только читать es_WantRetum Обеспечивает перевод строки по клавише Enter в много- строчном редакторе. Если редактор находится в диалого- вом блоке и стиль не установлен, нажатие клавиши Enter вызывает срабатывание кнопки по умолчанию. Независимо от того, установлен стиль es_WantReturn или нет, перевод строки может осуществляться по нажатии клавиш Ctrl+Enter DeleteSubText(StartPos, EndPos: Integer): Boolean - удаляет в редак- торе фрагмент текста от позиции StartPos до позиции EndPos; Undo - отменяет последнее действие; Paste - переносит текст из буфера обмена в редактор;
Гпава 6. Элементы управления в окнах 275 Сору - копирует помеченный текст из редактора в буфер обмена; Cut - переносит помеченный текст в буфер обмен, удаляя его в ре- дакторе; DeleteSelection: Boolean - удаляет помеченный текст; Scroll(HorizontalUnit, VerticalUnit: Integer) - скроллирует текст в окне редактора. Число символов по горизонтали и вертикали, на ко- торое перемещается текст, задают параметры HorizontalUnit и VerticalUnit. Положительные значения параметров означают скрол- линг текста вправо и вниз, а отрицательные - влево и вверх; Search(StartPos: Integer; AText: PChar; CaseSensitive: Boolean): Integer - осуществляет поиск в буфере редактора строки AText, начи- ная с позиции StartPos. Параметр CaseSensitive задает чувствитель- ность к прописным и строчным буквам при сравнении символов: если он равен False, то прописные и строчные буквы не различаются. Если искомый текст найден, то он помечается и Search возвращает его по- зицию; если нет, то Search возвращает -1. В объекте TEdit возможна проверка допустимости вводимого тек- ста (она работает только в однострочном редакторе). Проверку тек- ста выполняют специальные объекты, называемые валидаторами. Все они являются наследниками базового абстрактного типа TValidator и образуют отдельную иерархию объектов. Можно использовать сле- дующие типы валидаторов: • TFilterValidator - допускает ввод только заданного множества символов; • TRangeValidator - допускает ввод только целых чисел из задан- ного диапазона; • TStringLookupValidator - допускает ввод только определенных строк; • TPXPictureValidator - позволяет вводить текст по заданному шаблону (формат шаблона заимствован из СУБД Paradox). Для того чтобы организовать в редакторе проверку допустимости вводимых данных, нужно просто создать требуемый валидатор и ус- тановить его в объекте TEdit вызовом метода SetValidator. Например, если в редакторе вводится число и необходимо ограничить его мини- мальное и максимальное значения, после создания объекта редактора нужно записать: EditA.SetValidator(New(PRangeValidator, lnit(MinValue, MaxValue))); Теперь всякая попытка завершить работу с недопустимыми дан- ными будет отвергаться валидатором. Сообщения от редактора удобнее всего обрабатывать в его интер- фейсном объекте. Это связано с тем, что редактор является сложным управляющим элементом и использует в своей работе достаточно много типов сообщений, на каждый из которых желательно иметь
276 Программирование в среде Borland Pascal для Windows отдельный метод реакции. Примером служит метод ответа на сооб- щение wm_Char: TEdit = object(TStatic) procedure WMChar(var Msg: TMessage); virtual wm_First + wm_Char; end; TEdit перехватывает сообщения wm_Char и “пропускает” вводи- мые символы через валидатор (если он определен). Благодаря этому валидатор может осуществлять проверку строки не только при завер- шении ввода, но и в процессе ее набора. Такой способ проверки назы- вается фильтрацией. При использовании многострочного редактора в обычных (не диалоговых) окнах вы столкнетесь с одним маленьким неудобством: когда фокус ввода попадает на редактор, невозможно переместить его средствами клавиатуры на другой элемент управления (клавиши Tab и Shift+Tab работают по-другому). Между тем, в диалоговом окне редактор работает нормально. Это связано с тем, что обработка со- общений в диалоговых окнах отличается от обработки сообщений в обычных окнах. В окне диалога нажатия клавиш Tab и Shift+Tab приводят к перемещению фокуса ввода между управляющими элемен- тами соответственно вперед и назад. В обычных окнах за обработку сообщений от клавиатуры полностью отвечает программист. Если многострочный редактор находится в обычном окне, то виртуальный клавиатурный код vk_Tab преобразуется в символ табуляции. Чтобы по клавишам Tab и Shift+Tab фокус ввода перемещался от редактора к другому элементу управления, нужно в наследнике TEdit перекрыть виртуальный метод DefWndProc: procedure TMultiLineEdit.DefWndProc(var Msg: TMessage); begin if (Msg.Message = wm_KeyDown) and (Msg.WParam = vk_Tab) then if GetKeyState(vk_Shift) < 0 then SetFocus(PreviousA.HWindow) else SetFocus(NextA. HWindow) else inherited DefWndProc(Msg); end; Метод TMultiLineEdit.DefWndProc проверяет, является ли сообще- ние клавиатурным (Msg.Message = wm_KeyDown) и была ли нажата клавиша Tab (Msg.WParam = vk_Tab). В случае положительного отве- та фокус ввода перемещается на предыдущий или на следующий управляющий элемент в зависимости от того, нажималась клавиша Tab вместе с клавишей Shift (GetKeyState(vk_Shift) < 0) или нет. Во
Гпава 6. Элементы управления в окнах 277 всех остальных случаях вызывается стандартный обработчик сообще- ний базового объекта. Чтобы многострочный редактор работал совсем хорошо, устано- вим у него в атрибутах стиль es_WantReturn (объявлен в модуле WIN31): constructor TMultiLineEdit.lnit(AParent: PWindowsObject; Anld: Integer; ATitle: PChar; X, Y, W, H, ATextLen: Integer); begin inherited lnit(AParent, Anld, ATitle, X, Y, W, H, ATextLen, True); Attr.Style := Attr.Style or es_WantReturn; end; Стиль es_WantReturn разрешает в окне редактора перевод строки по нажатии клавиши Enter. Если редактор создается без этого стиля, то перевод строки осуществляется по нажатии клавиш Ctrl+Enter, а это, согласитесь, не совсем удобно. По умолчанию OWL не устанав- ливает стиль es_WantReturn в многострочном редакторе для совмес- тимости с Windows 3.0. 6.4.7. ОБЪЕКТ TListBox TListBox - это интерфейсный объект для блока списка. Блок списка создается на основе стандартного оконного класса ListBox и обеспе- чивает просмотр и выбор помещенных в него элементов. По умолча- нию элементами списка являются строки, но могут быть и другие объ- екты, в том числе графические. Элементы могут располагаться в одну или несколько колонок, автоматически сортироваться, скроллиро- ваться при просмотре. Особенности работы блока списка регулиру- ются специальными стилями из табл. 6.4. Конструктор Init объекта TListBox имеет следующий формат: TListBox.Init(AParent: PWindowsObject; Anld: Integer; X, Y, W, H: Integer); где AParent - адрес родительского оконного объекта; Anld - иденти- фикатор блока списка в родительском окне; X, Y - координаты левого верхнего угла блока списка; W, Н- ширина и высота блока списка. Конструктор InitResource наследуется у TControl, его формат: TListBox.InitResource(AParent: PWindowsObject; ResoiSrcelD: Word); где AParent - адрес родительского оконного объекта; ResourcelD - идентификатор блока списка в родительском окне. Если объект TListBox создается конструктором Init, в Attr.Style включается комплексный стиль lbs_Standard. Если применяется кон- структор InitResource, все атрибуты блока списка загружаются из ресурса.
278 Программирование в среде Borland Pascal для Windows Табл. 6.4. Стили блока списка Константа Описание lbs_Notify Информирует родительское окно о любых действи- ях пользователя со списком lbs_Sort lbs_NoRedraw Сортирует строки в окне списка по алфавиту Отменяет перерисовку блока списка при внесении в него изменений. Временная установка этого стиля позволяет избежать связанного с перерисовкой замедления при внесении в список множества изме- нений (перед работой со списком стиль устанавли- вается, а после работы сбрасывается) lbs_MultipleSel Разрешает выделить сразу несколько элементов списка. Выделение элемента устанавливается и снимается при любом щелчке мыши на нем lbs_OwnerDrawFixed Возлагает на владельца ответственность за перери- совку окна списка. Все элементы списка должны иметь одинаковую высоту lbs_OwnerDrawVariable Возлагает на владельца ответственность за перери- совку окна списка. Элементы списка могут иметь разную высоту lbs_HasStrings Используется с lbs_OwnerDrawFixed или lbs_OwnerDrawVariable и указывает, что список состоит из строк. При этом обеспечивается автома- тическое управление памятью для строк lbs_UseTabStops Позволяет блоку списка обнаруживать и обрабаты- вать символы табуляции. По умолчанию каждая позиция табуляции равна пустому промежутку шириной 32 “базовые единицы окна диалога”. Функция GetDialogBaseUnits позволяет узнать, сколько пикселов занимает единица окна диалога по ширине и высоте lbs_NoIntegralHeight Указывает, что размеры блока списка в точности равны размерам, заданным при создании управ- ляющего элемента (обычно Windows слегка подго- няет размеры окна списка так, чтобы в нем отобра- жалось целое число элементов) lbs_MultiColumn Позволяет располагать элементы списка в несколь- ко столбцов, которые могут прокручиваться по горизонтали. Ширина столбца устанавливается с помощью сообщения lb_SetColumnWidth lbs_WantKeyboard!nput Используется при сложной нестандартной работе со списком. Если этот стиль установлен, то при нажатии в блоке списка любой клавиши владелец получает сообщения wm_VKeyToItem и wm_CharTo!tem
Гпава 6. Элементы управления в окнах 279 Окончание табл. 6.4 Константа Описание lbs_ExtendedSel Позволяет выбирать в списке несколько элементов с помощью клавиши Shift и мыши или некоторой другой комбинации клавиш lbs_DisableNoScroll Отображает тусклую полосу скроллинга, если в списке недостаточно элементов lbs_Standard Комплексный стиль, состоящий из стилей lbs_Notify, lbs_Sort, ws_VScroll, ws_Border Методы TListBox обеспечивают гибкое управление списком из программы и используются для организации согласованной работы блока списка с другими элементами управления. Параметр Index во всех методах означает позицию элемента в списке, отсчитываемую от нуля. Все функции в случае ошибки возвращают отрицательное зна- чение. Перечислим эти методы: AddString(AString: PChar): Integer - добавляет в список строку AString и возвращает позицию, в которую строка была помещена. Если при этом установлен стиль lbs_Sort, то элементы автоматически сортируются; InsertString(AString: PChar; Index: Integer): Integer -.вставляет стро- ку в позицию, заданную параметром Index. Если в Index передается -1, то строка присоединяется к концу списка. Функция возвращает фак- тическую позицию вставленного элемента. При использовании InsertString список не пересортировывается; DeleteString(Index: Integer): Integer - удаляет из списка элемент с номером Index и возвращает число оставшихся элементов; ClearList - удаляет из списка все элементы; GetCount: Integer - возвращает число элементов в списке; GetString(AString: PChar; Index: Integer): Integer - копирует в буфер AString строку с номером Index и возвращает ее длину. Буфер AString должен иметь достаточный размер для приема строки; GetStringLen(Index: Integer): Integer - возвращает длину строки с номером Index (в длину строки не включается нуль-терминатор); GetSellndex: Integer - возвращает номер выделенной строки; SetSelIndex(Index: Integer): Integer - выделяет в списке строку с но- мером Index. Если значение Index равно -1, то существующее выделе- ние элементов снимается; GetSelString(AString: PChar; MaxChars: Integer): Integer - копирует в буфер AString выделенную в списке строку и возвращает ее длину. Параметр MaxChars указывает максимальное количество копируемых символов. Зарезервированный для строки буфер не должен быть меньше (MaxChars + 1);
280 Программирование в среде Borland Pascal для Windows SetSelString(AString: PChar; Index: Integer): Integer - выделяет в списке строку, совпадающую с AString. Поиск строки начинается с позиции Index. Если строка не найдена, то поиск повторяется, но уже с начала списка. Возвращается позиция выделенного элемента. Блок списка генерирует целый ряд уведомительных сообщений: lbn_DblClick, lbn_ErrSpace, lbn_KillFocus, lbn_SelChange, lbn_SetFocus. Они описаны в руководстве по Borland Pascal. 6.4.8. ОБЪЕКТ TComboBox Объект TComboBox порожден от TListBox и управляет комбинирован- ным блоком. Оконный класс комбинированного блока называется ComboBox и совмещает возможности списка и строкового редактора. Комбинированный блок можно наблюдать в окне выбора файла ин- тегрированной среды Borland Pascal (управляющий элемент с меткой File name). Конструкторы Init и InitResource объекта TComboBox схожи с конструкторами TListBox, но имеют дополнительный параметр ATextLen, который задает максимальную длину редактируемой стро- ки. Формат конструктора Init: TComboBox.Init(AParent: PWindowsObject; Anld: Integer; X, Y, W, H: Integer; AStyle, ATextLen: Word); где AParent - адрес родительского оконного объекта; Anld - иденти- фикатор комбинированного блока в родительском окне; X, Y - коор- динаты левого верхнего угла комбинированного блока; W,H - шири- на и высота комбинированного блока; AStyle - стили, добавляемые к Attr.Style; A TextLen - длина текстового буфера. Формат конструктора InitResource: TComboBox.InitResource(AParent: PWindowsObject; ResourcelD: Word; ATextLen: Word); где AParent - адрес родительского оконного объекта; ResourcelD - идентификатор комбинированного блока в родительском окне; A TextLen - длина текстового буфера. Специальные оконные стили, которые влияют на работу комбини- рованного блока, приведены в табл. 6.5. По умолчанию конструктор Init формирует комбинированный блок с сортированным списком (cbs_Sort) и скроллируемым редактором строки (cbs_AutoHScroll). Так как объекты комбинированного блока часто создаются с не- сколько различающимися стилями, в конструкторе Init введен пара- метр AStyle, который комбинируется с другими стилями оконного объекта.
Гпава 6. Элементы управления в окнах 281 Табл. 6.5. Стили комбинированного блока Константа Описание cbs_Simple Простейший вид комбинированного блока. Такой комбинированный блок постоянно отображает свой список. Текущий выбранный элемент списка дубли- руется в строке редактирования cbs_DropDown Комбинированный блок с выпадающим списком. Список отображается, когда пользователь нажима- ет справа от строки редактора кнопку со стрелкой. В остальном стиль cbs_DropDown аналогичен cbs_Simple cbs_DropDownList Комбинированный блок с выпадающим списком и только читаемой строкой редактирования, в кото- рой отображается выбранный элемент cbs_OwnerDrawFixed Возлагает на владельца ответственность за прори- совку комбинированного блока. Все элементы спи- ска должны иметь одинаковую высоту cbs_Owner Draw Variable Возлагает на владельца ответственность за прори- совку комбинированного блока. Элементы списка могут иметь разную высоту cbs.AutoHScroll Обеспечивает скроллинг текста в строке редактиро- вания вправо и влево. Если данный стиль не указан, ввести текст, превышающий окно редактора, невоз- можно cbs.OEMConvert Перед отображением на экране выполняется двой- ное преобразование текста: из таблицы ANSI в таблицу OEM и обратно. Это позволяет исключить ошибки при переводе полученных от пользователя символов в таблицу OEM. Стиль cbs_OEMConvert используется при вводе имени файла cbs_Sort cbs_HasStrings Сортирует строки в блоке списка по алфавиту Используется с cbs_OwnerDrawFixed или cbs_OwnerDrawVariable и указывает, что список состоит из строк. При этом обеспечивается автома- тическое управление памятью для строк cbs_N о I ntegral H eight Указывает, что размеры комбинированного блока в точности равны размерам, заданным при его созда- нии (обычно Windows слегка подгоняет размеры окна списка в комбинированном блоке так, чтобы в нем отображалось целое число элементов) cbs_DisableNoScroll Отображает тусклую полосу скроллинга, если в списке содержится недостаточное количество эле- ментов
282 Программирование в среде Borland Pascal для Windows Объект комбинированного блока содержит и методы управления списком, и методы управления строкой редактора: ShowList - отображает список ниспадающего (cbs_DropDownList) комбинированного блока; HideList - прячет список выпадающего (cbs_DropDownList) ком- бинированного блока; SetText(Str: PChar) - помещает в редактор строку Str; GetText(Str: PChar; MaxChars: Integer): Integer - копирует текст из редактора в буфер Str и возвращает его длину (без учета нуль- терминатора). Параметр MaxChars указывает максимальное число копируемых символов, включая нуль-терминатор. Если длина текста больше MaxChars, то лишние символы отбрасываются; GetTextLen: Integer - возвращает объем текста в редакторе и может использоваться перед вызовом GetText. Значение (GetTextLen + 1) соответствует объему памяти, который нужно зарезервировать для приема всего текста; Clear - удаляет в редакторе текст; SetEditSel(StartPos, EndPos: Integer): Integer - выделяет в редакторе текст, начиная с позиции StartPos и заканчивая EndPos; GetEditSel(var StartPos, EndPos: Integer): Boolean - возвращает в StartPos и EndPos начальную и конечную позиции выделенного тек- ста. Комбинированный блок генерирует целое множество уведоми- тельных сообщений: cbn_DblClk, cbn_DropDown, cbn_EditChange, cbn_EditUpdate, cbn_ErrSpace, cbn_KillFocus, cbn_SelChange, cbn_SelEndCancel, cbn_SelEndOk, cbn_SetFocus. Они подробно описа- ны в руководстве по Borland Pascal. 6.4.9. ОБЪЕКТ TScrollBar Объект TScrollBar служит для работы с полосой скроллинга - управ- ляющим элементом класса ScrollBar. Сразу оговоримся, что TScrollBar не обслуживает стандартные полосы скроллинга, присоединенные к окну с помощью стилей ws_HScroll и ws_VScroll. Создавая экземпляр TScrollBar, мы задаем в некоторых абстракт- ных единицах пределы перемещения ползунка, его начальную пози- цию, шаг одной строки и шаг одной страницы. Результатом работы полосы скроллинга является позиция ползунка на линейке. Формат конструктора Init: TScrollBar.Init(AParent: PWindowsObject; Anld: Integer; X, Y, W, H: Integer; IsHScrollBar: Boolean); где AParent - адрес родительского оконного объекта; Anld - иденти- фикатор полосы скроллинга в родительском окне; X,Y- координаты
Гпава 6. Элементы управления в окнах 283 левого верхнего угла полосы скроллинга; W\H- ширина и высота полосы скроллинга; IsHScrollBar - тип полосы скроллинга: горизон- тальная (True) или вертикальная (False). Формат конструктора InitResource: TScrollBar .InitResource(AParent: PWindowsObject; ResourcelD: Word); где AParent - адрес родительского оконного объекта; ResourcelD - идентификатор полосы скроллинга в родительском окне. Специальные оконные стили полосы скроллинга приведены в табл. 6.6. Табл. 6.6. Стили полосы скроллинга Константа Описание sbs_Horz Горизонтальная полоса скроллинга. Если не указан ни стиль sbs_TopAlign, ни стиль sbs_BottomAlign, полоса скроллинга имеет точные размеры, заданные при ее созда- нии sbs_Vert Вертикальная полоса скроллинга. Если не указан ни стиль sbs_LeftAlign, ни стиль sbs_RightAlign, полоса скроллинга имеет точные размеры^ заданные при ее создании sbs_Top Align Размещает горизонтальную полосу скроллинга по верхней границе прямоугольника, задающего ее размер. Полоса имеет заданную по горизонтали длину и стандартную ширину. Стиль sbs_TopAlign используется только с sbs_Horz sbs_LeftAlign Размещает вертикальную полосу скроллинга по левой границе прямоугольника, задающего ее размер. Полоса имеет заданную по вертикали длину и стандартную шири- ну. Стиль sbs_LeftAlign используется только с sbs_Vert sbs_BottomAlign Размещает горизонтальную полосу скроллинга по нижней границе прямоугольника, задающего ее размер. Полоса имеет заданную по горизонтали длину и стандартную ширину. Стиль sbs_BottomAlign используется только с sbs_Horz sbs_RightAlign Размещает вертикальную полосу скроллинга по правой границе прямоугольника, задающего ее размер. Полоса имеет заданную по вертикали длину и стандартную шири- ну. Стиль sbs_RightAlign используется только с sbs_Vert В объекте TScrollBar есть поля LineMagnitude и PageMagnitude ти- па Integer. LineMagnitude - это величина одной прокручиваемой стро- ки, a PageMagnitude - величина одной страницы. Они измеряются в абстрактных относительных единицах. По умолчанию LineMagnitude получает значение 1, a PageMagnitude - значение 10. Позиция ползун- ка и диапазон его перемещения в объекте TScrollBar не запоминаются,
284 Программирование в среде Borland Pascal для Windows а хранятся в управляющем элементе. По умолчанию начальная пози- ция равна 0, а диапазон - от 0 до 100. В объекте TScrollBar существуют следующие методы для управле- ния полосой скроллинга: GetRange(var LoVal, HiVal: Integer) - возвращает в переменных LoVal и HiVal нижнюю и верхнюю границы диапазона скроллинга; SetRange(LoVal, HiVal: Integer) - устанавливает новый диапазон скроллинга; GetPosition: Integer - возвращает текущую позицию ползунка; SetPosition(ThumbPos: Integer) - устанавливает ползунок в новую позицию; DeltaPos(Delta: Integer): Integer - перемещает ползунок относи- тельно текущей позиции на Delta единиц. Если смещение Delta поло- жительное, ползунок передвигается вправо (вниз), а если отрицатель- ное, - влево (вверх). В объекте TScrollBar уже определены методы ответа на сообщения полосы скроллинга, которые освобождают программиста от необхо- димости самому отслеживать ее состояние. Если в задачу входит пере- хват этих сообщений (например, с целью динамического изменения некоторого параметра), в родительском объекте определяется вирту- альный метод с селектором id_First и смещением, равным идентифи- катору полосы скроллинга. 6.5. РАБОТА С ДАННЫМИ 6.5.1. МЕХАНИЗМ ПЕРЕДАЧИ ДАННЫХ Основное назначение элементов управления - прием данных от поль- зователя в диалоговом режиме. Объекты OWL дают программисту возможность заполнять управляющие элементы данными перед нача- лом работы и считывать их после завершения. Чтобы организовать передачу данных, нужно выполнить следующие действия: • зарезервировать буфер, размер которого равен суммарному объ- ему редактируемых данных. Обычно это запись. Порядок следования полей в записи должен соответствовать последовательности управ- ляющих объектов в окне. Тип каждого поля определяется типом ре- дактируемых в соответствующем объекте данных (о типах данных мы поговорим чуть позже). В следующем примере объявлены данные для строки редактора и кнопки независимой фиксации: var Buffer: record EditField: array [0 .. 15] of Char; { данные для строки редактора } CheckField: Word; { данные для кнопки независимой фиксации } end;
Гпава 6. Элементы управления в окнах 285 • создать управляющие объекты и поместить их в окно. Объекты нужно формировать только для тех элементов управления, которые участвуют в передаче данных, например для зависимой и независимой кнопок, строки редактирования, блока списка и т.д. Причем у управ- ляющих объектов, создаваемых конструктором Init, нужно дополни- тельно вызвать метод EnableTransfer, чтобы разрешить прием и пере- дачу данных. Управляющих объектов для нажимающейся кнопки и блока группы формировать не надо, так как эти элементы не прини- мают и не возвращают никаких данных. Создавать управляющие объекты удобно в конструкторе их родительского оконного объекта, например: constructor TMyDialog.lnit(AParent: PWindowsObject; AName: PChar); var C: PControl; begin inherited lnit(AParent, AName); C := New(PEdit, lnitResource(@Self, id_EditField, SizeOf(Buffer.EditField)); C := New(PCheckBox, lnitResource(@Self, id_CheckField); end; • создать диалоговый (в общем случае оконный) объект и устано- вить в нем указатель TransferBuffer на буфер с данными, после этого выполнить диалог (открыть окно). Передача данных из буфера в управляющие элементы происходит автоматически, если указатель TransferBuffer не равен nil. Приведем пример создания и выполнения окна диалога: var D: PDialog; begin D := New(PMyDialog, lnit(ApplicationA.MainWindow, Transfer Test'); DA.TransferBuffer := ©Buffer; ApplicationA.ExecDialog(D); end; При завершении модального блока диалога кнопкой Ок данные автоматически возвращаютя в буфер. В других случаях приходится самостоятельно управлять перемещением данных, вызывая у оконно- го объекта метод © TransferData(Direction: Word); где Direction - направление передачи. Если значение параметра равно tf_SetData, то данные раздаются управляющим объектам, а если tf_GetData, - то данные собираются в буфер. В качестве примера использования TransferData приведем реали- зацию в объекте TDialog метода Ок:
286 Программирование в среде Borland Pascal для Windows procedure TDialog.Ok(var Msg: TMessage); begin if IsModal then begin if CanClose then begin T ransferData(tf_GetData); EndDlg(id_Ok); end; end else CloseWindow; end; Обращение к методу CanClose позволяет валидаторам (если они есть) проверить допустимость данных перед закрытием окна. 6.5.2. ТИПЫ ДАННЫХ Рассмотрим типы данных, с которыми работают управляющие объек- ты OWL. Объекты TButton и TGroupBox не обрабатывают данных. Данные объектов TCheckBox и TRadioButton имеют тип Word и могут принимать следующие значения: bf_Unchecked - пометка снята; bf_Checked - пометка установлена; bf_Grayed - неопределенное со- стояние (для кнопки независимой фиксации с тремя состояниями). Объекты TStatic и TEdit работают с массивом символов следующе- го вида: Text: array [0 .. maxLen] of Char; { maxLen - некоторая константа } В параметре ATextLen конструкторов TStatic и TEdit передается зна- чение SizeOf(Text) или maxLen + 1. Объект TListBox использует сложный формат данных. Для списка строк с единственным выбором он таков: TListBoxSingleRec = record Strings: PStrCollection; Index: Integer; end; где Strings - указатель на коллекцию ASCIIZ-строк; Index - номер выбранной строки. Для списка строк с множественным выбором данные имеют другой формат: TListBoxMultiRec = record Strings: PStrCollection; Indexes: PMultiSelRec; end; где Strings - указатель на коллекцию ASCIIZ-строк; Indexes - указа-
Гпава 6. Элементы управления в окнах 287 тель на запись типа TMultiSelRec с информацией о выбранных стро- ках. Формат записи таков: TMultiSelRec = record Count: Integer; Selections: array [0 .. 32760] of Integer; end; где Count - число строк в массиве Selections; Selections - номера строк. Реальный объем массива зависит от значения поля Count. Очевидно, что в секции var нельзя объявлять переменную типа TMultiSelRec, так как она требует сразу 64 Кбайт памяти. Запись TMultiSelRec нужно распределять в динамической памяти, используя функцию AllocMultiSel(Size: Integer): PMultiSelRec; где Size - число резервируемых в памяти элементов массива (эУо зна- чение заносится в поле Count). Для освобождения памяти вызывается функция FreeMultiSel(P: PMultiSelRec); Данные объекта TComboBox имеют следующую структуру: TComboBoxRec = record Strings: PStrCollection; Selection: array [0 .. maxLen] of Char; { maxLen - некоторая константа } end; где Strings - указатель на коллекцию нуль-терминированных строк, из которых состоит список; Selection - текст в редакторе (который может не совпадать ни с одним элементом коллекции Strings). Его длина (maxLen + 1) передается в параметре ATextLen конструкторов TComboBox.Init и TComboBox.InitResource. Объект TScrollBar работает с данными типа: JScrollBarTransferRec = record LowValue: Integer; HighValue: Integer; Position: Integer; end; где Low Value, HighValue - нижняя и верхняя границы диапазона скроллинга; Position - текущая позиция ползунка в эт&м диапазоне. 6.5.3. ПРИМЕР ВВОДА ДАННЫХ В ОКНЕ ДИАЛОГА Рассмотрим, как механизм передачи данных может быть использован при реализации модального диалога, изображенного на рис. 6.1 в начале главы.
288 Программирование в среде Borland Pascal для Windows Сначала создается файл ресурсов, в котором описывается простое меню и диалоговый блок. Меню имеет идентификатор idm_Menu и содержит единственный пункт Controls, который раскрывается в два других: Dialog (генерирует команду cm_ControlsDialog) и Exit (генерирует команду cm_Exit). Диалоговый блок (см. рис. 6.1) имеет в ресурсе идентификатор idd_ControlsDialog. Все идентификаторы ре- сурсов помещаются в отдельный INC-файл. Спроектированный диалоговый блок требует, чтобы буфер пере- дачи данных имел следующий вид: TControlsData = record CheckBox: Word; RadioButtonl: Word; RadioButton2: Word; { данные независимой кнопки CheckBox } { данные зависимой кнопки RadioButton 1} { данные зависимой кнопки RadioButton 2 } Edit: array [0..31] of Char; { данные редактора текста} ListBoxStrings: PStrCollection; { строки блока списка } ListBoxIndex: Integer; {выделенная строка блока списка} ComboBoxStrings: PStrCollection; { строки комбинированного блока } ComboBoxSelection: array [0..31] of Char; { выдел, строка комб.блока } ScrollBar: TScrollBarTransferRec; { данные полосы скроллинга } end; Буфер с данными обычно размещается в родителе диалогового ок- на. В нашем примере это главное окно программы: TMainWindow = object(TWindow) ControlsData: TControlsData; { данные для окна диалога Dialog} constructor lnit(ATitle: PChar); procedure CMControlsDialog(var Msg: TMessage); virtual cm_First + cm_ControlsDialog; procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; Далее объявляется наследник TDialog - объект TControlsDialog, в котором перекрывается конструктор Init. В конструкторе TControlsDialog.Init создаются интерфейсные объекты управляющих элементов, которые принимают и возвращают данные: constructor TControlsDialog.lnit(AParent: PWindowsObject; AName: PChar); var C: PControl; begin inherited lnit(AParent, AName); C := New(PCheckBox, lnitResource(@Self, id_CheckBox)); C := New(PRadioButton, lnitResource(@Self, id_RadioButton1)); C := New(PRadioButton, lnitResource(@Self, id__RadioButton2)); C := New(PEdit, lnitResource(@Self, id_Edit, 32)); C := New(PListBox, lnitResource(@Self, id_ListBox)); C := New(PComboBox, lnitResource(@Self, id_ComboBox, 32)); C := New(PScrollBar, lnitResource(@Self, id_ScrollBar)); end;
Гпава 6. Элементы управления в окнах 289 Экземпляр диалогового объекта TControlsDialog создается и за- пускается на выполнение при выборе команды меню Controls-Dialog: procedure TMainWindow.CMControlsDialog(var Msg: TMessage); var D: PDialog; begin D := New(PControlsDialog, lnit(@Self, PChar(idd_ControlsDialog))); DA.TransferBuffer := @ControlsData; if ApplicationA.ExecDialog(D) = id_Ok then lnvalidateRect(HWindow, nil, True); end; Если диалог завершается по нажатии кнопки Ок, запрашивается обновление рабочей области окна. При перерисовке в окне отобра- жаются текущие значения полей буфера передачи данных. Приведем полный текст программы CONTROLS: CONTROLS.INC const idm_Menu = 100; cm_ControlsDialog = 101; idd_ControlsDialog = 100; id_Button = 101; id_CheckBox = 102; id_RadioButton1 = 103; id_RadioButton2 = 104; id_Edit« = 105; idJJstBox = 106; id_ComboBox = 107; id_ScrollBar = 108; CONTROLS.PAS program Controls; {$R CONTROLS.RES} uses WinTypes, WinProcs, OWindows, ODialogs, Strings, Objects; {$1 CONTROLS.INC} type { Структура данных окна диалога "Dialog"} PControlsData = ATControlsData; TControlsData = record CheckBox: Word; RadioButton 1: Word; RadioButton2: Word; Edit: array [0..31] of Char; ListBoxStrings: PStrCollection; ListBoxIndex: Integer; ComboBoxStrings: PStrCollection; ComboBoxSelection: array [0..31] of Char; { выдел, строка комб.блока } ScrollBar: TScrollBarTransferRec; { данные полосы скроллинга } end; { данные независимой кнопки CheckBox } { данные зависимой кнопки RadibButton 1} { данные зависимой кнопки RadioButton 2 } { данные редактора текста } { строки блока списка } { выделенная строка блока списка } { строки комбинированного блока } 10 Зак. 1049
290 Программирование в среде Borland Pascal для Windows { Окно диалога "Dialog” с разнообразными управляющими элементами } PControlsDialog = ATControlsDialog; TControlsDialog = object(TDialog) constructor lnit(AParent: PWindowsObject; AName: PChar); end; { Гпавное окно программы } PMainWindow = ATMainWindow; TMainWindow = object(TWindow) ControlsData: TControlsData; { данные для окна диалога Dialog } constructor lnit(ATitle: PChar); procedure CMControlsDialog(var Msg: TMessage); virtual cm_First + cm_ControlsDialog; procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; { Прикладной объект } PControlsApp = ATControlsApp; TControlsApp = object(TApplication) procedure InitMainWindow; virtual; end; { Конструирует диалоговый объект и его дочерние управляющие объекты } constructor TControlsDialog.lnit(AParent: PWindowsObject; AName: PChar); var C: PControl; begin inherited lnit(AParent, AName); C := New(PCheckBox, lnitResource(@Self, id_CheckBox)); C := New(PRadioButton, lnitResource(@Self, id_RadioButton1)); C := New(PRadioButton, lnitResource(@Self, id_RadioButton2)); C := New(PEdit, lnitResource(@Self, id_Edit, 32)); C := New(PListBox, lnitResource(@Self, id_ListBox)); C := New(PComboBox, lnitResource(@Self, id_ComboBox, 32)); C := New(PScrollBar, lnitResource(@Self, id_ScrollBar)); end; { Конструирует оконный объект и инициализирует запись ControlsData } constructor TMainWindow.Init(ATitle: PChar); begin inherited lnit(nil, ATitle); Attr.Menu := LoadMenu(Hlnstance, PChar(idm_Menu)); with ControlsData do begin CheckBox := bfJJnchecked; RadioButton 1 := bf_Checked; RadioButton2 := bfJJnchecked; StrCopy(Edit, ’Edit’); ListBoxStrings := New(PStrCollection, lnit(6, 4)); ListBoxStringsA.lnsert(PChar('ListBox item 1')); ListBoxStringsA.lnsert(PChar('ListBox item 2')); ListBoxStringsA.lnsert(PChar('ListBox item 3')); ListBoxStringsA.lnsert(PChar('ListBox item 4')); ListBoxStringsA.lnsert(PChar('ListBox item 5')); ListBoxStringsA.lnsert(PChar('ListBox item 6'));
Гпава 6. Элементы управления в окнах 291 ListBoxIndex := 0; ComboBoxStrings := New(PStrCollection, lnit(6, 4)); ComboBoxStringsA.lnsert(PChar(’ComboBox item 1')); ComboBoxStringsA.lnsert(PChar('ComboBox item 2')); ComboBoxStringsA.lnsert(PChar('ComboBox item 3')); ComboBoxStringsA.lnsert(PChar('ComboBox item 4')); ComboBoxStringsA.lnsert(PChar('ComboBox item 5')); ComboBoxStringsA.lnsert(PChar('ComboBox item 6')); StrCopy(ComboBoxSelection, 'ComboBox'); with ScrollBar do begin LowValue := 0; HighValue := 100; Position := 0; end; end; end; { По команде Controls-Dialog открывает модальное окно диалога "Dialog”} procedure TMainWindow.CMControlsDialog(var Msg: TMessage); var D: PDialog; begin D := New(PControlsDialog, lnit(@Self, PChar(idd_ControlsDialog))); DA.TransferBuffer := ©Controls Data; if ApplicationA.ExecDialog(D) = id_Ok then lnvalidateRect(HWindow, nil, True); end; { Отображает в главном окне значения полей записи ControlsData } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var S1: String; S2: String [7]; S3: PChar; R1, R2: TRect; begin with R1 do begin Left := 0; Top := 20; Right := 150; Bottom := 40; end; with R2 do begin Left := 150; Top := 20; Right := 400; Bottom := 40; end; S1 := 'CheckBox :'; if ControlsData.CheckBox = bf_Checked then S2 := 'On' else S2 := 'Off; DrawText(PaintDC, @S1 [1], Length(S1), R1, dt_Right); DrawText(PaintDC, @S2[1], Length(S2), R2, dt_Left); OffsetRect(R1, 0, 20); OffsetRect(R2, 0, 20); * S1 := 'RadioButtonl :'; if ControlsData.RadioButtonl = bf_Checked then S2 := 'On' else S2 := 'Off; DrawText(PaintDC, @S1 [1], Length(S1), R1, dt_Right); DrawText(PaintDC, @S2[1], Length(S2), R2, dt_Left); OffsetRect(R1, 0, 20); OffsetRect(R2, 0, 20); S1 := 'RadioButton2 :';
292 Программирование в среде Borland Pascal для Windows if ControlsData. RadioButton2 = bf_Checked then S2 := ’On’ else S2 := ’Off; DrawText(PaintDC, @S1[1], Length(S1), R1, dt_Right); DrawText(PaintDC, @S2[1], Length(S2), R2, dt_Left); OffsetRect(R1, 0, 20); OffsetRect(R2, 0, 20); S1 := ’Edit: '; DrawText(PaintDC, @S1[1], Length(S1), R1, dt_Right); with ControlsData do DrawText(PaintDC, Edit, StrLen(Edit), R2, dt_Left); OffsetRect(R1, 0, 20); OffsetRect(R2, 0, 20); S1 := ’ListBox : ’; with ControlsData do S3 := PChar(ListBoxStringsA.At(ListBoxlndex)); DrawText(PaintDC, @S1 [1], Length(S1), R1, dt_Right); DrawText(PaintDC, S3, StrLen(S3), R2, dt_Left); OffsetRect(R1, 0, 20); OffsetRect(R2, 0, 20); S1 := ’ComboBox : ’; DrawText(PaintDC, @S1[1], Length(S1), R1, dt_Right); with ControlsData do DrawText(PaintDC, ComboBoxSelection, StrLen(ComboBoxSelection), R2, dt_Left); OffsetRect(R1,0, 20); OffsetRect(R2, 0, 20); ' S1 := 'ScrollBar (%): ’; DrawText(PaintDC, @S1 [1], Length(S1), R1, dt_Right); with ControlsData.ScrollBar do Str(Position, S2); DrawText(PaintDC, @S2[1 ], Length(S2), R2, dt_Left); end; { Создает объект главного окна программы } procedure TControlsApp.lnitMainWindow; begin MainWindow := New(PMainWindow, InitfControls')); end; var ControlsApp: TControlsApp; begin ControlsApp.lnitfControls'); ControlsApp. Run; ControlsApp. Done; end. На этом тему управляющих объектов можно считать исчерпанной. Предметом рассмотрения следующей главы является написание про- грамм с многооконным пользовательским интерфейсом.
7 ПРИЛОЖЕНИЯ С МНОГООКОННЫМ ИНТЕРФЕЙСОМ 7.1. ОСНОВЫ ОРГАНИЗАЦИИ MDI Многооконный интерфейс (от англ. Multiple Document Interface - MDI) - это стандарт пользовательского интерфейса для приложений Windows, который определяет способ работы одновременно с не- сколькими открытыми документами. Документом может быть (необязательно) содержимое некоторого файла на диске, например текстового файла, файла электронной таблицы и т.д. Program Manager и File Manager являются примерами программ с пользовательским интерфейсом MDI. В Program Manager вы можете одновременно управлять множеством программных групп, каждая из которых является документом. В программе File Manager документом является окно каталогов. Интегрированная среда разработки BPW также придерживается стандарта MDI. Перечислим основные принципы построения MDI-приложений в системе Windows: • все MDI-приложения имеют главное окно, называемое обрам- ляющим окном MDIyuwl окном рамки (frame window); • внутри обрамляющего окна располагаются дочерние окна MDI, содержащие документы. Если документ связан с файлом на диске, то его заголовок обычно содержит путь к этому файлу. С одним доку- ментом можно работать в нескольких дочерних окнах MDI. В этом случае в заголовке окна вместе с именем документа может отобра- жаться порядковый номер окна (например, minmdi.pas:l, minmdi.pas:2); • дочерние окна не видны за границами обрамляющего окна. При передвижении обрамляющего окна дочерние окна перемещаются вме- сте с ним. Минимизация и максимизация обрамляющего окна не из- меняют размеров его дочерних окон, содержащих документы. Однако если дочернее окно максимизировано, то оно . всегда занимает всю область обрамляющего окна. Минимизированные дочерние окна ото- бражаются внутри обрамляющего окна как пиктограммы; • в каждый момент времени только одно дочернее окно может быть активным. Активное дочернее окно помечается так же, как и окно рамки - цветовым выделением нерабочих областей окна. При
294 Программирование е среде Borland Pascal для Windows переключении на другое приложение Windows запоминает, какое до- чернее окно было активным, и при возврате в приложение восстанав- ливает его активность; • в отличие от программ с интерфейсом, ориентированным на один документ, MDI-приложение должно иметь меню, которое принадле- жит обрамляющему окну. Дочерние окна MDI не могут иметь своего меню. Управление всеми документами осуществляется из одного меню окна рамки; • меню окна рамки должно включать подменю управления дочер- ними окнами. Это подменю обычно называется Window и содержит такие команды как Cascade (Каскад), Tile (Мозаика), Arrange icons (Скомпоновать пиктограммы), Close all (Закрыть все). Windows авто- матически добавляет заголовок каждого открытого документа в ко- нец меню дочерних окон. Текущее активное окно помечается в этом списке “птичкой”. Windows может отображать в меню Window до девяти названий документов. Если одновременно открыто более девя- ти документов, то Windows добавляет в конец списка открытых окон команду More Windows.... При ее выборе на экране появляется окно диалога с заголовком Select Window, с помощью которого пользова- тель может активизировать любой из документов; • системное меню дочернего окна MDI содержит обычные коман- ды управления окном, но выбираются они с помощью клавиши Ctrl, а не Alt. Например, чтобы закрыть документ, нужно нажать клавиши Ctrl+F4. 7.2. КОМПОНЕНТЫ MDI-ПРИЛОЖЕНИЯ 7.2.1. ОБРАМЛЯЮЩЕЕ ОКНО Приложение, соответствующее стандарту MDI, имеет как минимум три типа окон: • обрамляющее окно MDI, выступающее главным окном приклад- ной программы; • окно-клиент, которое управляет дочерними окнами обрамляюще- го окна; • дочерние окна обрамляющего окна, содержащие документы. В основном интерфейс MDI обеспечивается только обрамляющим окном и окном-клиентом. В OWL обрамляющее окно представлено объектом TMDIWindow, который является наследником TWindow. Конструктор Init объекта TMDIWindow имеет два параметра: строку заголовка и дескриптор меню. Будучи главным окном программы, окно рамки не имеет роди- теля, поэтому в отличие от объекта TWindow в конструктор TMDIWindow.Init не передается указатель на родительское окно.
Гпава 7. Приложения с многооконным интерфейсом 295 Оконный объект типа TMDIWindow должен конструироваться в ме- тоде InitMainWindow прикладного объекта, например: procedure TMDIApp.lnitMainWindow; begin MainWindow := New(PMDIWindow, InitfMDI Application', LoadMenu(Hlnstance, PChar(IOO)))); end; В данном примере создается экземпляр объекта TMDIWindow с заго- ловком ’MDI Application' и загружаемым из ресурса меню. Предпола- гается, что меню имеет в ресурсе идентификатор 100. Так как объект TMDIWindow происходит из TWindow, он насле- дует все его поля и методы, в том числе указатель ChildList, который в среде MDI адресует дочерние окна, содержащие документы. Для дос- тупа к дочерним окнам MDI используются методы FirstThat, ForEach, Next, Previous. Дополнительно к полям TWindow объект TMDIWindow имеет два новых элемента данных: ChildMenuPos с типом Integer и ClientWnd с типом PMDIClient. Поле ChildMenuPos содержит порядковый номер пункта меню об- рамляющего окна, используемого окном-клиентом для ведения списка открытых документов. Конструктор TMDIWindow.Init первоначаль- но устанавливает ChildMenuPos в нуль, указывая самое левое подме- ню. В наследнике объекта TMDIWindow вы можете переустановить ChildMenuPos в другое значение, соответствующее позиции пункта Window вашего собственного меню. Поле ClientWnd адресует невидимое окно-клиент, которое принад- лежит обрамляющему окну и осуществляет закулисное управление документами MDI. Если вам потребуется выполнить какие-нибудь действия с окном-клиентом, его дескриптор можно получить через выражение GetClientA. HWindow. 7.2.2. ОКНО-КЛИЕНТ Внутри обрамляющего окна находится невидимое окно, называемое окном-клиентом или администратором MDI. Это окно занимает всю рабочую область обрамляющего окна и выполняет очень важную работу по управлению динамически создаваемыми дочерними окнами MDI. Роль окна-клиента является намного более значительной, чем это может показаться на первый взгляд. Если вы заметили, в MDI- приложении одновременно активны сразу два окна: обрамляющее окно и одно из его дочерних окон (активно окно или нет, можно су- дить по цвету его заголовка). Это необходимо, в частности, для того, чтобы обработку поступающих из меню команд могло выполнять не
296 Программирование е среде Borland Pascal для Windows только обрамляющее окно, в котором находится меню, но и активное (текущее в данный момент) дочернее окно. Windows не поддерживает состояние активности для дочерних окон со стилем ws_Child (именно такими являются дочерние окна MDI). Обеспечением активности до- чернего окна внутри обрамляющего занимается окно-клиент. Кроме того, на окно-клиент возлагается создание и удаление дочерних MDI- окон, контроль за переключением между документами (перемещение фокуса ввода между дочерними окнами). Расположение документов внутри обрамляющего окна “каскадом” или “мозаикой”, выравнива- ние пиктограмм осуществляет тоже окно-клиент. Функцию окна-клиента в библиотеке OWL выполняет объект TMDIClient. В предыдущих версиях OWL объект TMDIClient был наследником TControl. В настоящей версии, поставляемой с Borland Pascal 7.0, он порожден напрямую от объекта TWindow. В своих про- граммах вам не придется строить наследников объекта TMDIClient. Не стоит заботиться и о создании экземпляра TMDIClient, этим зани- мается обрамляющее окно. 7.2.3. ДОЧЕРНИЕ MDI-OKHA Дочерние MDI-окна являются обычными дочерними окнами (типа ws_Child) обрамляющего окна. Как правило, это экземпляры объекта TWindow или его наследников. Дочерние окна MDI управляют толь- ко своим содержимым и по возможности вообще не должны знать о том, является ли приложение многооконным. Дочерние MDI-окна создаются динамически обрамляющим окном по команде cm_CreateChild. Эта команда может быть закреплена в меню Window за пунктом Create. По команде cm_CreateChild объект TMDIWindow создает дочернее окно MDI, вызывая виртуальный метод CreateChild. Метод CreateChild в свою очередь вызывает вирту- альный метод InitChild для конструирования объекта дочернего окна MDI, а затем создает окно на экране с помощью метода MakeWindow прикладного объекта. Метод InitChild возвращает указатель на окон- ный объект и по умолчанию создает экземпляр TWindow с заголовком ’MDI Child’. Таким образом, для того чтобы по команде cm_CreateChild созда- вались экземпляры других объектов, нужно объявить наследника TMDIWindow и перекрыть в нем метод InitChild. 7.3. ПРОСТЕЙШЕЕ MDI-ПРИЛОЖЕНИЕ Приведенная ниже программа MINMDI является простейшим MDI- приложением. Ее окно изображено на рис. 7.1.
Гпава 7. Приложения с многооконным интерфейсом 297 Рис. 7.1. Окно простейшего MDI-приложения program MinMDI; {$R MINMDI.RES} uses WinTypes, WinProcs, OWindows; const idm_Menu = 100; type TMDIApp = object(TApplication) procedure InitMainWindow; virtual; end; procedure TMDIApp.lnitMainWindow; begin MainWindow := New(PM DI Window, InitfMDI Application', LoadMenu(Hlnstance, PChar(idm_Menu)))); end; var MDIApp: TMDIApp; begin MDIApp.lnit('MinMDI'); MDIApp.Run; MDIApp. Done; end. В программе MINMDI мы не определяем новые типы оконных объектов, а используем стандартные объекты библиотеки OWL.
298 Программирование е среде Borland Pascal для Windows Главное окно создается в методе InitMainWindow прикладного объек- та TMDIApp и является экземпляром объекта TMDIWindow. Меню верхнего уровня содержит единственный пункт Window, в котором определены команды Create, Cascade, Tile, Arrange icons, Close all. Отметим, что числовые идентификаторы этих команд объявлены в модуле OWINDOWS и имеют зарезервированные значения. Описание стандартных команд MDI содержится в табл. 7.1. Табл. 7.1. Стандартные команды MDI Константа Описание cm_MDIFileNew Создать новый документ cm_M DI FileOpen Открыть существующий документ cm_Arrange!cons Скомпоновать (упорядочить) пиктограммы cm_TileChildren Расположить окна без перекрытий cm_CascadeChildren Расположить окна каскадом cm_CloseChildren Закрыть все дочерние окна MDI cm_CreateChild Открыть новое окно В OWL уже предусмотрена реакция на большинство стандартных команд интерфейса MDI, поэтому в обрамляющем объекте не требу- ется определять процедуры их обработки. По команде меню Window-Create объект TMDIWindow автомати- чески создает новый экземпляр TWindow с заголовком MDI Child. Чтобы по команде Create создавались объекты другого типа, объяви- те наследника TMDIWindow и перепишите в нем метод InitChild. 7.4. МНОГООКОННЫЙ ТЕКСТОВЫЙ РЕДАКТОР Типичным примером MDI-приложения является многооконный тек- стовый редактор. В библиотеке OWL редактирование текстового файла осуществля- ет объект TFileWindow (модуль OSTDWNDS). Если мы попытаемся сделать экземпляр TFileWindow главным окном приложения, то полу- чим программу, очень похожую на “записную книжку” (Notepad) из группы Accessories (Реквизиты). Иначе говоря, объект TFileWindow реализует однооконное редактирование текстовых файлов. Чтобы наш редактор был многооконным, нужно просто создать обрамляющее окно, в котором дочерними оконными объектами будут экземпляры TFileWindow. Отметим, что такая технология проектиро- вания MDI-приложений, когда вначале создается однооконное при- ложение, а потом оно делается многооконным, является очень эффек- тивной. Мы советуем вам при решении задачи не разрабатывать сразу
Гпава 7. Приложения с многооконным интерфейсом 299 обрамляющее MDI-окно, а написать и отладить программу в одно- оконном варианте. Если программа спроектирована правильно, то надстройка многооконного интерфейса, возможно, даже не потребует переделки существующего кода. Однако следует помнить, что не каж- дое приложение может быть многооконным, по аналогии с тем, как не каждое приложение может запускаться одновременно в нескольких копиях. Объект TFileWindow уже умеет реагировать на основные команды меню File: New, Open..., Save, Save as.... Однако обработка им команд New и Open... ориентирована на однооконное редактирование. В од- нооконном приложении при выборе в меню File пункта New объекту главного окна посылается команда cm_FileNew. В ответ на эту коман- ду объект TFileWindow очищает окно редактора и назначает ему за- головок по умолчанию (Untitled). Многооконный текстовый редактор должен реагировать на команду New иначе. Он должен создавать но- вое дочернее окно редактора. По команде Open... многооконное при- ложение должно загружать выбранный текстовый файл не в текущее активное окно, а в новое окно редактора. Для того чтобы не изменялся смысл команд cmJFileNew и cmJFileOpen при переходе от однооконного к многооконному прило- жению, в модуле OWINDOWS объявлены команды cm_MDIFileNew и cm_MDIFileOpen. В многооконном приложении именно эти команды должны быть закреплены за пунктами New и Open... файлового меню. Обработка этих команд возлагается на обрамляющее окно. Разработанная нами программа MDIEDIT реализует многоокон- ное редактирование текстовых файлов. program MDIEdit; {$R MDIEDIT. RES} uses WinTypes, WinProcs {$ifdefVer80}, Messages {$endif}, WinDos, Strings, OWindows, OStdDIgs, OStdWnds, BWCC; const idm_Menu = 100; CmdSubCount = 15; CmdSubSet: array [1..CmdSubCount] of Word = ( cm_FileSave, cm_FileSaveAs, cm_Arrangelcons, cmjnieChildren, „ cm_CascadeChildren, cm_CloseChildren, cm_EditCut, cm_EditCopy, cm_EditPaste, cm_EditDelete, cm_EditClear, cm_EditUndo,
300 Программирование в среде Borland Pascal для Windows cm_EditFind, cm_EditReplace, . cm_EditFindNext); type PMainWindow = ATMainWindow; TMainWindow = object(TM DI Window) constructor lnit(ATitle: PChar); procedure ToggleCommand(Command: Word; OnOff: Boolean); procedure UpdateCommandSet; procedure CMMDIFileNew(var Msg: TMessage); virtual cm_First + cm_MDIFileNew; procedure CMMDIFiieOpen(var Msg: TMessage); virtual cm_First + cm_MDIFileOpen; end; TMDIEditApp = object(TApplication) function IdleAction: Boolean; virtual; procedure InitMainWindow; virtual; end; { Конструирует оконный объект и запоминает позицию меню Window} constructor TMainWindow.lnit(ATitle: PChar); begin inherited lnit(ATitle, LoadMenu(Hlnstance, PChar(idm_Menu))); ChildMenuPos := 3; end; { Разрешает или запрещает заданную команду меню } procedure TMainWindow.ToggleCommand(Command: Word; OnOff: Boolean); var State: Word; begin If OnOff then State := mf_ByCommand or mf_Enabled else State := mf_ByCommand or mf_Grayed; EnableMenultem(Attr.Menu, Command, State); end; { Управляет множеством доступных команд меню } procedure TMainWindow.UpdateCommandSet; var W: HWnd; OnOff: Boolean; I: Integer; begin W := HWnd(SendMessage(GetClientA.HWindow, wm_MDIGetActive, 0, 0)); ifW = 0then OnOff := False else OnOff := True; for I := 1 to CmdSubCount do ToggleCommand(CmdSubSet[l], OnOff); end;
Глава 7. Приложения с многооконным интерфейсом 301 { Создает новое пустое окно редактора по команде меню File-New} procedure TMainWindow.CMMDIFileNew(var Msg: TMessage); begin ApplicationA.MakeWindow(New(PFileWindow, lnit(@Self,", ”))); end; { Загружает файл в новое окно редактора по команде меню File-Open } procedure TMainWindow.CMMDIFileOpen(var Msg: TMessage); var FileName: array [O..fsPathName] of Char; begin if ApplicationA.ExecDialog(New(PFileDialog, Init(@Self, PChar(sd_FileOpen), StrCopy(FileName, '*.*’)))) = id_Ok then ApplicationA.MakeWindow(New(PFileWindow, lnit(@Self,", FileName))); end; { Обновляет множество доступных команд в холостом цикле программы } function TMDIEditApp.ldleAction: Boolean; begin IdleAction := False; PMainWindow(MainWindow)A.UpdateCommandSet; end; { Создает объект главного окна программы } procedure TMDIEditApp.lnitMainWindow; begin MainWindow := New(PMainWindow, InitfMDI Editor’)); end; var MDIEditApp: TMDIEditApp; begin MDIEditApp.lnit(’MDIEdit’); MDIEditApp.Run; MDIEditApp.Done; end. Чтобы сделать редактор многооконным, мы объявили главное ок- но TMainWindow наследником TMDIWindow и определили в нем методы ответа на команды cm_MDIFileNew и cm_MDIFileOpen. По команде cm_MDIFileNew в объекте TMainWindow создается новый дочерний объект TFileWindow с заголовком по умолчанию. По ко- манде cm_MDIFileOpen выполняется диалоговое окно, в котором у пользователя запрашивается имя файла, и, если диалог был завершен по кнопке Ок, создается окно редактора TFileWindow, связанное с этим файлом. Меню в текстовом редакторе работает таким образом, что если в обрамляющем MDI-окне не открыто ни одного окна редактора, то в нем доступны только команды File-New, File-Open и File-Exit. Недос- тупные команды отображаются в меню серым цветом. Обновление списка доступных команд производится периодически из метода IdleAction объекта TMDIEditApp. Виртуальный метод
302 Программирование в среде Borland Pascal для Windows IdleAction вызывается прикладным объектом в цикле обработки со- общений, если в очереди приложения отсутствуют другие сообщения. Он позволяет организовать в программе фоновое выполнение корот- ких операций. Мы подробно рассматривали назначение и использо- вание метода IdleAction в 5.4.5. Напомним, что по умолчанию метод IdleAction объекта TApplication ничего не делает и возвращает False, сигнализируя о том, что до появления сообщений в очереди его вызы- вать не надо. В прикладном объекте TMDIEditApp метод IdleAction вызывает метод UpdateCommandSet главного окна программы, который фак- тически и управляет набором доступных команд. Метод UpdateCommandSet анализирует, существует ли в обрамляющем окне хотя бы один открытый документ. Если да, то команды из массива CmdSubSet делаются в меню доступными, в противном случае - не- доступными. Для того чтобы проверить, существуют ли открытые документы, окну-клиенту посылается сообщение wm_MDIGetActive. В ответ на это сообщение окно-клиент возвращает дескриптор активного дочер- него окна MDI. Если в обрамляющем окне не открыто ни одного до- кумента, то возвращается нулевой дескриптор. Это обстоятельство используется в методе UpdateCommandSet для определения режима разрешения или запрещения команд. Непосредственное переключение пункта меню из разрешенного со- стояния в запрещенное и наоборот выполняет метод ToggleCommand объекта TMainWindow.
8 ВЫВОД НА ПРИНТЕР 8.1. ОСНОВНЫЕ ПОЛОЖЕНИЯ Вывод информации на устройство печати осуществляется с помощью тех же функций GDI, которые используются для рисования на экране дисплея. Для печати, как и при выводе на дисплей, создается связан- ный с принтером контекст устройства (DC). Его дескриптор передает- ся при вызове различных GDI-функций, создающих изображение. Принтер, как устройство вывода, очень сильно отличается от дис- плея по физическим принципам работы и по параметрам. Печать все- гда производится на некотором носителе, например на бумаге, при этом каждый повторный вывод порождает новую копию документа (ее часто называют твердой копией). По сравнению с выводом на дис- плей печать является длительным процессом, во время которого воз- можны ошибки (например, отсутствие бумаги), разного рода сбои. Кроме того, процесс печати может быть приостановлен со стороны пользователя или вообще прерван, качество изображения, получае- мого на лазерных и струйных принтерах, как правило, значительно превосходит качество того же изображения, воспроизводимого на экране дисплея. В связи с перечисленными особенностями организация печати яв- ляется достаточно трудоемким делом и требует от программиста до- полнительных усилий по управлению принтером, его настройке, об- работке ошибок и других ситуаций (несмотря на унифицированность GDI-интерфейса). К счастью, библиотека OWL содержит набор объ- ектов, которые упрощают взаимодействие с принтером и позволяют наделить программу средствами печати ценой минимальных усилий. 8.2. ПЕЧАТЬ СРЕДСТВАМИ OWL Все объекты, которые так или иначе связаны с выводом на принтер, расположены в модуле OPRINTER. Следовательно, этот модуль дол- жен обязательно подключаться к программе с помощью оператора uses. С модулем OPRINTER связан также файл ресурса OPRINTER.RES, который должен быть доступен компилятору. На
304 Программирование в среде Borland Pascal для Windows рис. 8.1 приведены две диаграммы, отражающие иерархию объектов модуля OPRINTER. Рис. 8.1. Иерархия объектов модуля OPRINTER Объект TPrinter отождествляется с физическим устройством печати и обеспечивает интерфейс между прикладной программой и драйве- ром принтера. Такие действия, как выбор принтера, его настройка и печать, осуществляются через обращения к методам объекта TPrinter. Пользоваться объектом TPrinter очень удобно, поскольку он уже уме- ет создавать и выполнять все необходимые диалоговые блоки, связан- ные с выбором и настройкой принтера, а также выдачей сообщений об ошибках. Обычно программа содержит один экземпляр объекта TPrinter, ко- торым владеет объект главного окна: TMainWindow = object(TWindow) Printer: PPrinter; constructor lnit(AParent: PWindowsObject; ATitle: PChar); destructor Done; virtual; end; Объект типа TPrinter должен создаваться в конструкторе главного окна и уничтожаться в деструкторе: constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); Printer := New(PPrinter, Init); end;
Гпава 8. Вывод на принтер 305 destructor TMainWindow. Done; begin if Printer <> nil then Dispose(Printer, Done); inherited Done; end; Во многих программах (например, в IDE BPW) меню File содержит пункты Printer Setup... и Print.... По команде Printer Setup... на экране отображается блок диалога, в котором можно выбрать принтер из числа доступных и установить аппаратно-зависимые параметры печа- ти (управление списком доступных устройств печати обеспечивается утилитой Printers, запускаемой из Control Panel). Команда Print... в меню File позволяет напечатать текущий открытый документ. При этом пользователю может быть предварительно предложено задать диапазон печатаемых страниц, количество копий и некоторые другие параметры. Рассмотрим, как это реализовать в программе. Действия, выполняемые по команде Printer Setup..., уже встроены в объект TPrinter, поэтому все, что необходимо сделать при реакции на эту команду, - это вызвать Setup у экземпляра объекта TPrinter: procedure TMainWindow.CMFilePrinterSetup(var Msg: TMessage); begin PrinterA.Setup(@Self); end; Метод Setup создает и выполняет окно диалога TPrinterSetupDlg (рис. 8.2). Он имеет единственный параметр - указатель на оконный объект, назначаемый владельцем окна выбора принтера. Для того чтобы распечатать текст или графику, например по ко- манде File-Print..., необходимо вызвать у экземпляра объекта TPrinter метод Print. Метод Print подготавливает и запускает процесс печати; это самый важный и сложный метод объекта TPrinter. При вызове ему передаются два параметра: указатель на оконный объект (обычно указатель на объект главного окна), а также указатель на объект типа TPrintOut. Оконный объект используется как владелец диалоговых Рис. 8.2. Окно выбора принтера
306 Программирование в среде Borland Pascal для Windows окон, которые создаются и выполняются при^вызове метода Print. Объект TPrintOut отождествляется с распечаткой и осуществляет не- посредственный вывод информации на принтер с помощью функций GDI. Для выполнения конкретной процедуры печати обязательно создается потомок этого объекта. Приведем пример возможной реакции на команду cm_FilePrint, ге- нерируемую пунктом меню File-Print...: procedure TMainWindow.CMFilePrint(var Msg: TMessage); var P: PPrintOut; begin P := New(PMyPrintOut, InitfSome Document')); Printer7*. Print(@Self, P); Dispose(P, Done); end; В примере создается временный объект типа TMyPrintOut (предполагается, что он является наследником TPrintOut). В его кон- структор передается название документа (в примере - 'Some Document'). Это название используется, в частности, утилитой Print Manager как идентификатор документа в очереди активных работ. Далее у объекта Printer вызывается метод Print, которому в качестве объекта распечатки передается экземпляр TMyPrintOut. После завер- шения печати экземпляр объекта TMyPrintOut уничтожается. В результате вызова у объекта Printer метода Print на экране появ- ляется окно подготовки к печати, которое изображено на рис. 8.3. В этом окне пользователю предлагается указать диапазон печатае- мых страниц, количество копий, а также, следует ли сортировать ко- пии. Если документ должен печататься так, что сначала создается Рис. 8.3. Окно подготовки к печати
Гпава 8. Вывод на принтер 307 первая копия, затем вторая и так далее, то кнопка Collate Copies должна быть включена. В противном случае, т.е. если сначала должны создаваться все страницы с номером 1, затем все страницы с номером 2 и так далее, кнопка Collate Copies должна быть выключена. После того как пользователь установил требуемые параметры и нажал кнопку Ок, запускается процесс печати. Во время этого процес- са на экране отображается немодальное диалоговое окно, предназна- ченное для экстренного прекращения печати (экземпляр объекта TPrinterAbortDlg). Чтобы прервать печать, нужно нажать кнопку Cancel - единственный элемент управления, присутствующий в этом диалоговом окне. Метод Print объекта TPrinter управляет лишь процессом печати; конкретные действия по выводу информации на принтер выполняет объект TPrintOut, точнее его наследник. В примере эта роль возложе- на на объект TMyPrintOut. Каждый потомок TPrintOut обеспечивает печать документов опре- деленного типа. В нем обязательно должен быть перекрыт виртуаль- ный метод PrintPage, являющийся своего рода аналогом метода Paint оконного объекта TWindow: TPrintOut.PrintPage(Page: Word; var Rect: TRect; Flags: Word); virtual; где Page - текущий номер страницы, которую следует распечатать (страницы нумеруются от 1); Rect - размеры текущей страницы или ее части; Flags - различные флаги, сообщающие о выбранных парамет- рах печати (табл. 8.1). Флаги носят информационный характер и ред- ко используются. Если в объекте TPrintOut булевское поле Banding имеет значение False (состояние по умолчанию), то параметры Rect и Flags должны игнорироваться. Табл. 8.1. Флаги метода PrintPage Константа Описание pf_Graphics Печать графики pf_Text Печать текста pf_Both Печать графики и текста (равносильно pf_Graphics or pf_Text) pf_Banding Печать страницы по частям. Для того ч^обы это было возможно, элементу данных Banding объекта TPrintOut должно быть присвоено значение True pf_Selection Печать выделенной части документа, например отмечен- ного в текстовом редакторе блока Метод PrintPage предназначен для вывода на принтер указанной страницы документа средствами GDI. В качестве контекста устройст-
308 Программирование в среде Borland Pascal для Windows ва при вызове GDI-функций должен передаваться элемент данных DC объекта TPrintOut. Запись Size типа TPoint, которая тоже объявлена в объекте TPrintOut, позволяет судить о размерах выводимой страницы в единицах устройства (в пикселах). При многостраничной печати в наследнике объекта TPrintOut не- обходимо также перекрыть виртуальный метод: TPrintOut.HasNextPage(Page: Word): Boolean; virtual; где Page - номер страницы. Метод HasNextPage вызывается после каждого обращения к PrintPage. Он должен возвращать значение типа Boolean, показывающее, существует ли следующая страница для печа- ти (ее номер передается в параметре Page). Метод HasNextPage объек- та TPrintOut просто возвращает False, указывая тем самым на то, что печатается только одна страница. Перекрывая виртуальный метод GetSelection, можно обеспечить печать выделенной части документа. Если документ имеет отмечен- ный блок, значение GetSelection должно быть True, а в параметрах Start и Stop должны возвращаться номера начальной и конечной страниц, содержащих выделенный блок. В зависимости от значения, полученного в результате вызова GetSelection, в диалоговом окне подготовки к печати (см. рис. 8.3) будет доступна или недоступна кнопка зависимой фиксации Selection. Виртуальные методы BeginPrinting и EndPrinting позволяют осуще- ствлять дополнительную инициализацию и деинициализацию процес- са печати. Они вызываются один раз соответственно до и после печа- ти независимо от количества копий документа. Для инициализации и деинициализации печати каждой копии используются виртуальные методы BeginDocument и EndDocument. По умолчанию методы BeginPrinting, EndPrinting, BeginDocument и EndDocument в объекте TPrintOut ничего не делают. Приводимая далее программа PRNTEST демонстрирует вывод на принтер некоторого изображения, содержащего графику и текст. Графическая часть состоит из 6 концентрических квадратов и такого же числа вписанных в них окружностей. Расстояние между контурами соседних квадратов и окружностей - 1 см. Текст состоит из последо- вательности ASCII-символов с кодами от 32 до 127, разделенных про- белами: PRNTEST.INC const idm_Menu = 100; cm_FilePrint = 105; cm_FilePrinterSetup = 106; PRNTEST.PAS program PrnTest;
Гпава 8. Вывод на принтер 309 {$R PRNTEST.RES} uses WinTypes, WinProcs, OWindows, OPrinter, Strings, BWCC; {$1 PRNTEST.INC} type PTestPrintout = ATTestPrintout; TTestPrintout = object(TPrintOut) procedure PrintPage(Page: Word; var Rect: TRect; Flags: Word); virtual; end; PMainWindow = ATMainWindow; TMainWindow = object(TWindow) Printer: PPrinter; { объект, связанный с принтером } constructor lnit(AParent: PWindowsObject; ATitle: PChar); destructor Done; virtual; procedure CMFilePrint(var Msg: TMessage); virtual cm_First + cm_FilePrint; procedure CMFilePrinterSetup(var Msg: TMessage); virtual cm_First + cm_FilePrinterSetup; procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; end; PPmTestApp = ATPrnTestApp; TPrnTestApp = object(TApplication) procedure InitMainWindow; virtual; end; { Выводит в заданный контекст устройства графику и текст } procedure Draw(DC: HDC); var R: TRect; I: Integer; Ch: Char; S: String; OldMapMode: Integer; OldBrush: HBrush; begin OldMapMode := SetMapMode(DC, mm_LoMetric); OldBrush := SelectObject(DC, GetStockObject(Null_Brush)); R.Left := 0; R.Top := 0; R.Right := 1100; R.Bottom := -1100; MoveTo(DC, R.Left, R.Top); LineTo(DC, R.Right, R.Bottom); MoveTo(DC, R.Left, R.Bottom); LineTo(DC, R.Right, R.Top); for I := 1 to 6 do begin » Rectangle(DC, R.Left, R.Top, R.Right, R.Bottom); Ellipse(DC, R.Left, R.Top, R.Right, R.Bottom); lnflateRect(R, -100,100); end; S := "; for Ch := #32 to #127 do S := S + ’' + Ch; R.Left := 0; R.Top := -1200; R.Right := 1100; R.Bottom := r1600;
310 Программирование в среде Borland Pascal для Windows DrawText(DC, @S[1], Length(S), R, dt_Left or dt_WordBreak); SelectObject(DC, OldBrush); SetMapMode(DC, OldMapMode); end; { Печатает страницу документа } procedure TTestPrintout.PrintPage(Page: Word; var Rect: TRect; Flags: Word); begin Draw(DC); end; { Конструирует оконный объект и создает объект, связанный с принтером } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); Attr.Menu := LoadMenu(Hlnstance, PChar(idm_Menu)); Printer := New(PPrinter, Init); if Printer = nil then Status := emJnvalidMainWindow; end; { Уничтожает объект, связанный с принтером, и разрушает оконный } destructor TMainWindow. Done; begin if Printer <> nil then Dispose(Printer, Done); inherited Done; end; { По команде меню File-Print запускает печать } procedure TMainWindow.CMFilePrint(var Msg: TMessage); var P: PPrintOut; begin P := New(PTestPrintout, Init(Attr.Title)); PrinterA.Print(@Self, P); Dispose(P, Done); end; { По команде File-Printer Setup открывает окно выбора принтера } procedure TMainWindow.CMFilePrinterSetup(var Msg: TMessage); begin PrinterA.Setup(@Self); end; { Рисует изображение в контексте дисплея } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); begin Draw(PaintDC); end; { Создает объект главного окна программы } procedure TPrnTestApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Print Test’)); end; var PrnTestApp: TPrnTestApp;
Гпава 8. Вывод на принтер 311 begin PrnT estApp. I nitfPmT est’); PrnTestApp.Run; PrnTestApp.Done; end. Обратите внимание на то, что для вывода изображения на дисплей и принтер используется одна и та же процедура Draw. Для обеспече- ния точности метрических размеров изображения вне зависимости от разрешающей способности устройства вывода процедура Draw уста- навливает с помощью функции SetMapMode режим масштабирования mm_LoMetric. В этом режиме одной логической единице соответству- ет 0.1 мм. Это означает, что для отображения, например, прямоуголь- ника со сторонами 200 мм и 100 мм, нужно вызвать функцию Rectangle со следующими параметрами: Rectangle(DC, Х1, Y1, Х1 + 2000, Y1 + 1000); Следует помнить, что в режиме масштабирования mm_LoMetric рисование происходит в декартовой системе координат: ось X на- правлена вправо, а ось Y - вверх, что отличается от направления, принятого по умолчанию. 8.3. ДОПОЛНИТЕЛЬНЫЕ ВОЗМОЖНОСТИ МОДУЛЯ OPRINTER Иногда в программе требуется распечатать содержимое некоторого окна. Для этого не нужно создавать своих потомков объекта TPrintOut. Лучше воспользоваться уже существующим в модуле OPRINTER объектом TWindowPrintout, который как раз для этого и предназначен. При создании экземпляра этого объекта, одним из па- раметров конструктора передается указатель на окно, содержимое которого выводится на принтер. В объекте TWindowPrintout объявле- но поле Scale типа Boolean. Если оно имеет значение True (по умолча- нию), то изображение может масштабироваться в сторону сжатия или увеличения, занимая всю страницу. Перед запуском печати вы можете присвоить полю Scale значение False. В этом случае масштабирование не производится, а не уместившаяся на странице часть изображения отбрасывается. Еще один наследник TPrintOut - объект TEditPrintout, определен- ный в модуле OPRINTER, обеспечивает печать содержимого тексто- вого редактора. С этой целью в его конструктор передается указатель на объект TEdit. Отметим, что объект TEditPrintout позволяет печа- тать отмеченный в редакторе текстовый блок. При этом в диалоговом окне подготовки к печати становится доступной кнопка Selection.
312 Программирование в среде Borland Pascal для Windows Использование объектов TWindowPrintout и TEditPrintout не пред- ставляет особой сложности, поэтому приводить примеры с их участи- ем мы не будем. При организации печати часто требуется расширенное управление принтером и более тесное с ним взаимодействие. Возможно, что вы также пожелаете видоизменить диалоговые окна выбора и настройки принтера, подготовки к печати и ее прерывания. Все это можно сде- лать с помощью полей и методов объекта TPrinter. Поле Status определяет состояние объекта TPrinter; его возможные значения приведены в табл. 8.2. Табл. 8.2. Значения поля Status объекта TPrinter Константа Описание ps_Ok Ассоциация с принтером установлена, и объект готов для печати ps_InvalidDevice Установлены неверные параметры устройства, которые не позволяют установить ассоциацию с драйвером принтера ps_Unassociated Ассоциация с принтером не установлена Вместо интерактивного выбора принтера с помощью рассмотрен- ного ранее метода Setup вы можете напрямую назначить устройство вывода. Для этого нужно вызвать метод SetDevice и передать ему со- ответствующие параметры: TPrinter.SetDevice(ADevice, ADriver, APort: PChar); где ADevice - нуль-терминированная строка, которая содержит имя устройства, поддерживаемого драйвером принтера (как правило, один драйвер поддерживает несколько моделей принтеров конкрет- ной фирмы); ADriver - нуль-терминированная строка, содержащая имя драйвера для устройства печати; APort - порт вывода. Имя драй- вера, передаваемое в параметре ADriver, должно состоять только из имени файла, исключая расширение DRV и маршрут. Драйверы уст- ройств обычно располагаются в каталоге C:\WINDOWS\SYSTEM. В качестве значений параметра APort могут передаваться строки ’PRN:’, ’СОМ1:’, ’LPT2:’ и т.д. Если в параметре APort передается ’FILE:’, то вывод направляется в файл на диске. Его имя запрашивается системой Windows перед началом печати. Например, чтобы направить вывод на принтер Hewlett-Packard LaserJet 4V, который подключен к первому параллельному порту компьютера, необходимо поместить текст программы строку: PrinterA.SetDevice(’HP LaserJet 4V/4MV’, ’HPPCL5E’, ’LPT1:’);
Гпава 8. Вывод на принтер 313 При создании объект TPrinter настраивается для вывода на прин- тер по умолчанию (Default Printer). Его параметры - имя устройства, имя драйвера и порт вывода - хранятся в файле WIN.INI в разделе [windows] в строке, которая начинается с device=. Данные установки не следует изменять вручную: удобнее сделать это с помощью утилиты Printers, расположенной в Control Panel. С помощью обращения к методу Configure можно предложить пользователю настроить выбранный принтер: Printer\Configure(Application\MainWindow); Если в программе требуется использовать нестандартные диалого- вые окна установки принтера, запуска и остановки печати, то нужно объявить наследника объекта TPrinter и переопределить в нем вирту- альные методы InitSetupDialog, InitPrintDialog и InitAbortDialog. По умолчанию они создают соответственно экземпляры объектов TPrinterSetupDlg, TPrintDialog и TPrinterAbortDlg, возвращая их в качестве значения. Переопределив виртуальный метод ReportError, можно изменить реакцию объекта TPrinter на ошибки. Код ошибки хранится в поле Error. В завершение обзора средств печати остается добавить, что Windows сняла проблему распечатки какой бы то ни было информа- ции раз и навсегда.
9 ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ 9.1. ОБЩИЕ ПОЛОЖЕНИЯ Если проанализировать код большого количества прикладных про- грамм, то можно обнаружить, что они используют много совершенно одинаковых функций. Так, практически все программы подключают стандартные библиотеки систем программирования, а программы, которые обращаются к базам данных, - функции используемой СУБД (Система Управления Базой Данных). Дублирование кода, которое возникает с увеличением числа программ, заставляет пользователя расширять дисковую и оперативную память компьютера. Возможный выход из данной ситуации состоит в использовании динамически подключаемых библиотек - DLL (от англ. Dynamic Link Library). Динамически подключаемая библиотека представляет собой испол- няемый модуль системы Windows (обычно имеющий расширение DLL), код и ресурсы которого могут использоваться другими дина- мическими библиотеками и приложениями. Если обычная статическая библиотека подключается к программе на этапе компоновки и со- ставляет неотъемлемую часть исполняемого ЕХЕ-файла, то динамиче- ская библиотека представляет собой самостоятельный модуль и под- ключается к программе во время ее исполнения. Windows загружает DLL-библиотеки отдельно от программ, поэтому несколько приложе- ний могут совместно использовать одну и ту же копию библиотеки в памяти. Кроме того, динамические библиотеки загружаются в память лишь при необходимости и могут выгружаться из нее, освобождая ресурсы для других библиотек и приложений. Чтобы подчеркнуть значение, придаваемое DLL-библиотекам в Windows, отметим одну интересную деталь - сама операционная сис- тема Windows почти полностью состоит из динамических библиотек. Модули KERNEL, USER, GDI и др., файлы шрифтов (расширение FOT) и драйверов (расширение DRV) - все это динамически подклю- чаемые библиотеки. Использование DLL-библиотек является нормой практически для всех коммерческих приложений. Приложения, создаваемые на Borland Pascal, могут использовать DLL-библиотеки, разработанные с помощью других языков и систем
Глава 9. Динамически подключаемые библиотеки 315 программирования. Справедливо также и обратное утверждение - DLL-библиотеки, разработанные на Borland Pascal могут использо- ваться другими системами программирования. Обращения в программе к функциям динамической библиотеки ничем не отличаются от обращений к собственным функциям. Функ- ции DLL-библиотеки могут вызывать друг друга, а также функции других DLL-библиотек, которые в свою очередь могут осуществлять обратные вызовы. Допустимы любые по сложности рекурсии. DLL-библиотека похожа на модуль языка Borland Pascal - она также является библиотекой подпрограмм. Однако подключение DLL-библиотеки происходит не во время компиляции, а во время исполнения. Когда вы компилируете программу, которая вызывает функции DLL-библиотеки, то компилятор строит вызовы к так назы- ваемой библиотеке импорта, состоящей из крошечных фрагментов кода - переходников (thunks), связывающих программу с соответст- вующими функциями динамической библиотеки. При запуске прикладной программы на исполнение загрузчик опе- рационной оболочки Windows узнает из заголовка исполняемого файла, какие DLL-библиотеки должны быть подключены, и загружа- ет их в оперативную память (этот процесс называется динамическим связыванием). Для каждой загруженной динамической библиотеки Windows ведет так называемый счетчик привязок (use counter), кото- рый показывает, сколько приложений одновременно работают с дан- ной библиотекой. Если в процессе динамического связывания обна- руживается, что DLL-библиотека уже присутствует в памяти, то за- грузчик не создает ее новый экземпляр, а увеличивает на 1 счетчик привязок библиотеки. Когда прикладная программа завершается, счетчики привязок всех используемых ею DLL-библиотек уменьша- ются на 1. Динамическая библиотека, у которой счетчик привязок становится равным 0, выгружается из оперативной памяти. Так же, как и прикладная программа, динамическая библиотека имеет сегмент данных и сегменты кода, но она не имеет своего сегмен- та стека! В качестве стека DLL-библиотека использует стек вызываю- щей прикладной программы. Как известно, обычные Windows про- граммы совмещают сегмент стека с сегментом данных (DS = SS). Так вот, для DLL-библиотек имеет место как раз обратная ситуация (DS <> SS). У программистов на языке С данное обстоятельство вы- зывало в свое время немало проблем, связанных с ра&работкой собст- венных DLL-библиотек и использованием в них стандартных библио- течных функций языка С. К счастью, язык Borland Pascal лишен не- достатков такого рода. Лучший способ познакомиться с DLL-библиотеками - это разра- ботать собственную динамическую библиотеку, а затем подключить ее к прикладной программе.
316 Программирование в среде Borland Pascal для Windows 9.2. НАПИСАНИЕ DLL-БИБЛИОТЕК По структуре исходный текст библиотеки похож на исходный текст программы, за исключением следующих трех особенностей: • текст любой DLL-библиотеки должен начинаться с заголовка, который состоит из ключевого слова library, за которым следует имя библиотеки (аналогично заголовку программы, состоящему из клю- чевого слова program и имени программы). Однако если для програм- мы заголовок можно опустить, то для DLL-библиотеки заголовок является обязательным; он указывает компилятору на создание файла динамической библиотеки с расширением DLL; • все функции динамической библиотеки, которые предполагается вызывать из других приложений, должны быть объявлены с ключе- вым словом export. Например: function MyFunc(X, Y: Integer): Integer; export; Директива export делает функцию экспортируемой и заставляет ком- пилятор, во-первых, использовать “дальнюю” (far) модель вызова, а во- вторых, сгенерировать для данной функции специальные входной и выходной коды - пролог и эпилог (prolog & epilog). Данная директива должна указываться при первом же упоминании функции и не может использоваться совместно с директивой forward. Если функция объяв- лена с директивой export, то это означает, что ее можно экспортиро- вать, но она не обязательно будет экспортирована, т.е. она может быть и недоступна для программ и других DLL-библиотек; • функции, которые экспортирует DLL-библиотека, должны быть перечислены в разделе exports. Функции, упомянутые в разделе exports, должны быть ранее объявлены с ключевым словом export. Разделов exports может быть несколько, и они могут располагаться в программе произвольным образом. Пункты раздела exports разделя- ются запятой, а весь раздел заканчивается точкой с запятой. Если по какой-то причине вы не поместили в текст динамической библиотеки раздел exports, то ни одна из ее функций не будет экспортирована. Каждая экспортируемая функция, хранимая в динамической биб- лиотеке, идентифицируется порядковым номером {индексом) и стро- ковым ключом {именем). При запуске прикладной программы на вы- полнение индексы и имена используются для поиска соответствующих функций и настройки адресов точек входа в DLL-библиотеку. Индекс функции представляет собой положительное целое число в диапазоне от 1 до 32767. В качестве имени может выступать любая последова- тельность символов; имена функций в DLL-библиотеках “чувствительны” к прописным и строчным буквам. Раздел exports позволяет указать, какие функции динамической библиотеки, с какими индексами и именами должны быть экспорта-
Гпава 9. Динамически подключаемые библиотеки 317 рованы. В следующем примере перечислены некоторые возможные варианты экспортирования функций: exports MyFuncl, MyFunc2 name ’МуОупаРипсг', MyFunc3 index 3, MyFunc4 index 4 name ’MyDynaFunc4’, MyFunc5 index 5 name ’MyDynaFunc5’ resident; Если индекс или имя экспортирования для функции не указаны, то компилятор назначает их автоматически: в качестве индекса прини- мается порядковый номер функции, а в качестве имени - имя самой функции, преобразованное к прописным буквам. Например, функции MyFuncl по умолчанию назначается имя ’MYFUNC1’. Если индекс при экспортировании функции не указан, то вы не вправе делать ка- кие бы то ни было предположения относительно индекса, назначенно- го компилятором по умолчанию, и будете вынуждены импортировать такую функцию только по имени. Поэтому мы рекомендуем всегда задавать индексы явно. В разделе exports к имени каждой экспортируемой функции может быть добавлено ключевое слово resident. Структура DLL-файла пре- дусматривает наличие специальных таблиц для хранения имен экс- портируемых функций. Если для функции задана директива resident, то компилятор помещает ее имя в таблицу резидентных имен DLL- файла, в противном случае - в таблицу нерезидентных имен. Таблица резидентных имен в отличие от таблицы нерезидентных имен всегда загружается в оперативную память вместе с кодом DLL-библиотеки и остается там вплоть до ее выгрузки, поэтому директива resident позво- ляет ускорить поиск точек входа в функции по именам (но только для тех функций, для которых задана эта директива). Однако при этом менее эффективно используется оперативная память. В теле динамической библиотеки могут быть объявлены типы, константы, переменные, функции, процедуры, объекты и другие эле- менты, но экспортировать она может только функции и процедуры. Доступ к данным DLL-библиотеки следует осуществлять с использо- ванием процедурного интерфейса. Приведем пример простейшей динамической библиотеки MATH, которая экспортирует всего две функции - Min и Мах; { Заголовок динамической библиотеки } library Math; { Функция, возвращающая минимальное из двух чисел } function Min(X, Y: Integer): Integer; export; begin if X < Y then Min := X else Min := Y; end;
318 Программирование в среде Borland Pascal для Windows { Функция, возвращающая максимальное из двух чисел } function Мах(Х, Y: Integer): Integer; export; begin if X > Y then Max := X else Max := Y; end; { Раздел экспорта } exports Min, Max; { Блок инициализации динамической библиотеки } begin end. DLL-библиотека MATH удовлетворяет всем минимальным требо- ваниям: она имеет заголовок, начинающийся с ключевого слова library; функции Min и Мах объявлены с директивой export и опреде- лены в разделе exports. В примере MATH используется простейший вариант синтаксиса раздела exports, при котором функции экспорти- руются по своим же именам. Определение DLL-библиотеки заканчивается главным оператор- ным блоком begin ... end, внутри которого вы можете выполнить лю- бые действия, связанные с инициализацией библиотеки. Данный опе- раторный блок будет выполнен один раз при загрузке DLL- бйблиотеки в оперативную память. Динамическая библиотека MATH не требует инициализации, поэтому ее главный операторный блок пустой. Если DLL-библиотека требует выполнения каких-либо действий перед выгрузкой из памяти (деинициализацией), то это можно сделать так же, как и в обычной программе, вставив свою процедуру в цепоч- ку процедур выхода (для этого нужно переопределить значение указа- теля ExitProc). Инициализацию и завершение работы динамической библиотеки мы рассмотрим позже. 9.3. ИСПОЛЬЗОВАНИЕ DLL-БИБЛИОТЕК 9.3.1. СПОСОБЫ ИМПОРТА Для того чтобы функции DLL-библиотеки стали доступны в при- кладной программе, они должны быть подключены или, говоря ина- че, импортированы. Импорт функций динамической библиотеки мо- жет осуществляться двумя различными способами: • с помощью директивы компилятора external (статический им- порт)', • “вручную” с использованием функций LoadLibrary и GetProcAddress (динамический импорт).
Гпава 9. Динамически подключаемые библиотеки 319 Статический импорт является более удобным и используется на- много чаще. 9.3.2. СТАТИЧЕСКИЙ ИМПОРТ В случае статического импорта в исходный текст программы необхо- димо поместить внешние объявления функций динамической библио- теки, которые предполагается вызывать. В Borland Pascal функция может быть объявлена как внешняя с помощью директивы компиля- тора external, синтаксис которой был расширен для подключения функций DLL-библиотек. Например, для того чтобы использовать в программе функции Min и Мах динамической библиотеки MATH, достаточно поместить в тело программы следующие объявления: function Min(X, Y: Integer): Integer; far; external ’MATH’; function Max(X, Y: Integer): Integer; far; external ’MATH’; После этого функции Min и Max можно использовать так, как буд- то они являются частью самой прикладной программы. Программа USEMATH демонстрирует подключение и использование динамиче- ской библиотеки MATH: program UseMath; uses WinCrt; function Min(X, Y: Integer): Integer; far; external 'MATH'; function Max(X, Y: Integer): Integer; far; external 'MATH'; begin WriteLn(’max(min(3, 8), min(9, 5)) equals: Max(Min(3, 8), Min(9, 5))); end. Код DLL-библиотеки располагается отдельно от сегментов кода программы, которая ее вызывает, поэтому все функции DLL- библиотеки используют дальнюю модель вызова. Это обстоятельство должно быть непременно отражено в объявлении импортируемой функции. Для этого вы можете либо воспользоваться директивой компилятора {$F+}, либо указать в заголовке функции модификатор far непосредственно перед словом external (как в приведенном выше примере). Существует три вида статического импорта функций динамиче- ской библиотеки: • по имени; * • по новому имени, которое имеет функция в DLL-библиотеке; • по индексу. Следующий пример демонстрирует импорт некоторых абстракт- ных функций по имени, по новому имени и по индексу: function MyFunc1(X, Y: Integer): Integer; far; external ’MYDLL’; { по своему имени }
320 Программирование в среде Borland Pascal для Windows function MyFunc2(X, Y: Integer): Integer; far; external 'MYDLL* name ’MyDynaFunc2’; { по новому имени} function MyFunc3(X, Y: Integer): Integer; far; external 'MYDLL* index 3; { по индексу} Если функция импортируется по имени, то в качестве ключа поис- ка этой функции в DLL-библиотеке принимается имя, которое она имеет в программе, но записанное прописными буквами. Функции Min и Мах в примере USEMATH импортируются по своим именам. Мы советуем вам использовать статический импорт по именам только во время разработки и отладки динамической библиотеки, т.е. тогда, когда список функций и их номера могут изменяться. В оконча- тельной версии стоит все-таки использовать импорт по индексам, так как он наиболее эффективен по скорости. Обратите внимание на то, что на момент компиляции ЕХЕ- программы DLL-библиотеки, которые она подключает, могут еще и не существовать. Следовательно, разработка программ и динамиче- ских библиотек может осуществляться совершенно независимо; кроме того, перекомпиляция DLL-библиотеки или ее замена на новую вер- сию не требует перекомпиляции ни одной прикладной программы. 9.3.3. ДИНАМИЧЕСКИЙ ИМПОРТ Действия, которые при статическом импорте выполняет загрузчик DLL-библиотеки, вы можете проделывать “вручную”, обращаясь к соответствующим функциям Windows. Для этого необходимо напи- сать программный код, который загружает DLL-библиотеку в па- мять, а затем получает один за другим адреса точек входа в ее функ- ции. Эти адреса можно сохранить, а затем использовать для вызова функций динамической библиотеки. Загрузить DLL-библиотеку в память позволяет функция LoadLibrary(LibFileName: PChar): THandle; где LibFileName - указатель на нуль-терминированную строку, содер- жащую имя файла динамической библиотеки. При успешном завер- шении функция возвращает дескриптор загруженной DLL- библиотеки. Если параметр LibFileName содержит только имя файла без маршрута, то Windows пытается найти библиотеку в других ката- логах в соответствии со следующими приоритетами: • в текущем каталоге; • в каталоге системы Windows (там, где расположен файл WIN.COM); • в системном каталоге Windows (там, где расположен, например, файл GDI.EXE);
Гпава 9. Динамически подключаемые библиотеки 321 • в каталоге, который содержит исполняемый файл программы, вызвавшей функцию LoadLibrary; • во всех каталогах, перечисленных в переменной среды PATH; • в списке сетевых каталогов. Если файл динамической библиотеки не найден или имеет место любая другая ошибка, то значение дескриптора, возвращенное функ- цией LoadLibrary, находится в диапазоне от 0 до 32. Код загрузки динамической библиотеки MATH может выглядеть так: var LibHandle: THandle; begin LibHandle := LoadLibraryfMATH.DLL’); if LibHandle < 32 then Error; end; Если при вызове LoadLibrary модуль DLL-библиотеки уже загру- жен, то Windows не загружает его заново, создавая новую копию, а увеличивает на 1 счетчик привязок библиотеки. В противном случае файл DLL-библиотеки считывается с диска и загружается в оператив- ную память. Счетчик привязок динамической библиотеки увеличива- ется на 1 при каждом вызове функции LoadLibrary. После завершения работы с библиотекой ее необходимо освобо- дить вызовом функции FreeLibrary(LibHandle); где LibHandle - дескриптор DLL-библиотеки. Функция уменьшает на 1 счетчик привязок динамической библиотеки и, если он становится равным 0, полностью выгружает ее из оперативной памяти. Функции LoadLibrary и FreeLibrary можно вызывать для одного и того же мо- дуля произвольное число раз, но эти вызовы должны быть сбаланси- рованы. После загрузки динамической библиотеки вы можете воспользо- ваться функцией GetProcAddress для получения адресов функций, которые она экспортирует: GetProcAddress(Module: THandle; ProcName: PChar): TFarProc; где Module - дескриптор библиотечного модуля, который содержит функцию; ProcName - указатель на имя функции или ее порядковый номер (индекс). Функция GetProcAddress возвращает дальний указатель на проце- дуру с именем ProcName в DLL-библиотеке, идентифицируемой деск- риптором Module. Если функцию следует искать по имени, то в пара- метре ProcName передается указатель на нуль-терминированную И Зак. 1049
322 Программирование в среде Borland Pascal для Windows строку, содержащую имя функции; если же функцию следует искать по номеру, то в двух младших байтах параметра ProcName передается ее номер (положительное целое число в диапазоне от 1 до 32767), а старшие два байта должны быть равны 0. Функция с именем ProcName может и не существовать в динамиче- ской библиотеке. В этом случае GetProcAddress возвратит nil. Однако если вы пытаетесь получить адрес функции не по имени, а по номеру, то значение, возвращенное функцией GetProcAddress, будет не равно nil, даже если функции с таким номером нет. Из этого следует вывод: если в программе возможна ситуация, когда функция DLL- библиотеки отсутствует, поиск следует производить по имени. С помощью функции GetProcAddress вы можете получить только адрес функции, но не информацию о количестве и типе ее параметров, поэтому при динамическом импорте очень удобными оказываются процедурные типы Borland Pascal. Например, процедурный тип функ- ции Min может быть объявлен так: type TMin = function (X, Y: Integer): Integer; Для хранения адреса функции DLL-библиотеки объявите в про- грамме переменную этого типа: var Min: TMin; Если теперь присвоить переменной Min результат вызова GetProcAddress, то ее можно будет использовать в дальнейшем для вызова функции динамической библиотеки: @Min := GetProcAddress(LibHandle, ‘MIN’); Обратите внимание на то, что правила согласования типов языка Borland Pascal требуют указания символа @ перед переменной Min. После этого очень просто вызвать функцию Min динамической библиотеки MATH: А := Min(B, С); Для того чтобы вы могли сравнить технологии статического и ди- намического импорта, мы приводим программу USEMATH2, которая функционально аналогична программе USEMATH, но использует вместо статического импорта динамический: program UseMath2; uses WinTypes, WinProcs, WinCrt; type TMin = function (X, Y: Integer): Integer; TMax = function (X, Y: Integer): Integer;
Гпава 9. Динамически подключаемые библиотеки 323 const { Заголовок окна сообщений об ошибках } Caption = 'Application Execution Error1; var LibHandle: THandle; { дескриптор динамической библиотеки } Min: TMin; {указатель на функцию вычисления минимума } Мах: ТМах; {указатель на функцию вычисления максимума } begin { Загружаем динамическую библиотеку } LibHandle := LoadLibrary('MATH.DLL'); { Проверяем, нормально ли загружена библиотека } if LibHandle < 32 then begin MessageBox(0, 'Cannot load Dynamic Link Library.', Caption, mb_Ok or mb_TaskModal or mbJconExclamation); Halt(255); end; { Получаем адрес функции вычисления минимума } @Min := GetProcAddress(LibHandle, 'MIN'); { Получаем адрес функции вычисления максимума } @Мах := GetProcAddress(LibHandle, 'МАХ'); { Проверяем корректность указателей } if (@Min = nil) or (@Max = nil) then begin MessageBox(0, 'Cannot link to procedure in DLL.', Caption, mb_Ok or mbJaskModal or mbJconExclamation); FreeLibrary(LibHandle); Halt(255); end; { Теперь можно вызывать функции Min и Max} WriteLn('max(min(3, 8), min(9, 5)) equals:', Max(Min(3, 8), Min(9, 5))); { Освобождаем библиотеку } FreeLibrary(LibHandle); end. Как вы могли убедиться, динамический импорт в отличие от ста- тического менее удобен и требует больших усилий при реализации. Но он также имеет и ряд достоинств, среди которых выделим следую- щие: • более эффективное использование ресурсов оперативной памяти по той причине, что DLL-библиотеку можно загружать и выгружать из памяти по мере надобности; © • программе предоставляется полный контроль над подключением DLL-библиотек. Динамический импорт удобно применять тогда, ко- гда подключаемые функции могут отсутствовать в DLL-библиотеке и требуется особая обработка ошибок доступа к библиотеке и ее проце- дурам. Обычно в форме DLL-библиотек очень удобно реализовывать и подключать замещаемые драйверы. В этом случае динамический импорт оказывается незаменимым.
324 Программирование в среде Borland Pascal для Windows 9.4. МНОГОМОДУЛЬНЫЕ И ВЗАИМОЗАВИСИМЫЕ ДИНАМИЧЕСКИЕ БИБЛИОТЕКИ Рассмотренные выше примеры демонстрируют лишь самые простые подходы к составлению и подключению DLL-библиотек. Эти подхо- ды становятся неудобными или вообще перестают “работать”, когда требуется разработать сложную многомодульную динамическую биб- лиотеку, функции которой могут вызывать друг друга, а также функ- ции других динамических библиотек. Ниже мы приводим правила, которых полезно придерживаться при разработке любых - и простых, и самых сложных - динамических библиотек на языке Borland Pascal: • вся динамическая библиотека должна быть представлена как со- вокупность модулей (в частном случае, один-единственный модуль) языка Borland Pascal. Функции и процедуры, которые могут быть экспортируемы, должны быть объявлены в интерфейсной части с ключевым словом export; • главный файл DLL-библиотеки (начинающийся с заголовка library) должен содержать раздел uses, который подключает все необ- ходимые модули, раздел exports, в котором перечислены экспортируе- мые функции, а также операторный блок begin ... end, который ини- циализирует динамическую библиотеку. Не забывайте, что каждый из модулей библиотеки тоже может иметь секцию инициализации, кото- рая вызывается перед тем, как будет вызван главный инициализаци- онный блок; • все внешние объявления функций динамической библиотеки должны быть вынесены в отдельный модуль, называемый модулем импорта или библиотекой импорта. Чтобы подключить DLL- библиотеку, необходимо указать этот модуль в списке используемых модулей раздела uses программы. Давайте теперь разработаем динамическую библиотеку, которая продемонстрирует практическое использование описанных выше пра- вил. Для того чтобы наша библиотека имела хотя бы небольшое смы- словое содержание, поместим в нее две функции - Soundex и LetterSet. Поясним назначение этих функций. Функция Soundex позволяет получить звуковой код для некоторо- го слова или предложения. Ее результат может использоваться для сравнения строк по звучанию. Функция LetterSet возвращает четырехбайтное число, каждый бит которого соответствует определенной букве английского алфавита и показывает, встречается ли эта буква в строке, переданной как пара- метр при вызове LetterSet. Мы предоставляем вам возможность самостоятельно разобраться с алгоритмами работы функций Soundex и LetterSet.
Гпава 9. Динамически подключаемые библиотеки 325 Функции Soundex и LetterSet оформлены как отдельный модуль STRUNIT.PAS: unit StrUnit; interface { Возвращает звуковой код для некоторого слова или предложения. Может использоваться для сравнения строк по звучанию } function Soundex(S: String): String? export; { Возвращает четырехбайтное число, каждый бит которого соответствует определенной букве английского алфавита и показывает, встречается ли эта буква в строке, переданной в параметре } function LetterSet(S: String): Longint; export; implementation function Soundex(S: String): String; const SoundexTable: array [Char] of Char = ( #0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0, #0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0, #o,#o,#o,#o,#o,#o,#o,#o,#o,#o,#o,#o,#o,#o,#o,#o,#o,#o, #0.#0.#0.#0.#0.#0.#0.#0.#0.#0.#0. {ABCDEFGHI J К L M #0, T, ‘2’, '3‘, #0, ’T, ’2’, #0, #0, ’2‘, ’2‘, ‘4’, '5', {NOPQRSTUVWXYZ •5’, #0, T, *2’, ’б’, *2’, '3‘, #0, T, #0, ’2‘, #0, ’2‘, #0,#0,#0,#0,#0,#0, } } } } { a b c d e f 9 h / j k / m #0, T, *2’, ’3’, #0. ’1’, *2’, #0. #0. ’2’, ’2’, ’4’, ’5‘, { n 0 P Q r s t и V w X У z *5’, #0. ’1’, *2’, •6’, '2‘, *3’, #0. •г, #0. ‘2’, #0, '2‘, #0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0, #0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0, #0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0, #0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0, #o,#o,#o,#ol#o,#o,#o,#o,#o,#o,#ol#o,#o,#o,#o,#o,#ol#o, #o,#o,#o,#o,#o,#o,#o,#o,#o,#o,#o>#o,#or#o,#o,#o,#o,#o, #0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0,#0, #0,#0,#0,#0,#0,#0,#0); var Res: String[4]; StrPos, ResPos: Integer; PrevCode, Code: Char; begin Res := *0000’; if Length(S) > 0 then begin Res[1] := UpCase(S[1]); StrPos := 2; ResPos := 2; PrevCode := #0; while (StrPos <= Length(S)) and (ResPos <= 4) do
326 Программирование в среде Borland Pascal для Windows begin Code := SoundexTable[S[StrPos]]; if (Code > #0) and (Code <> PrevCode) then begin Res[ResPos] := Code; Inc(ResPos); end; Inc(StrPos); end; end; Soundex := Res; end; function LetterSet(S: String): Longint; var I: Integer; Res: Longint; begin Res := 0; for I := 1 to Length(S) do if S[l] in [*A*..*Z*, a . z] then Res := Res or (Longint(1) shl (Byte(UpCase(S[l])) - Byte(*A*))); LetterSet := Res; end; end. • Модуль STRUNIT подключается в главном файле STRDLL. Ком- пиляция последнего приводит к созданию результирующего файла динамической библиотеки STRDLL.DLL: library StrDII; uses StrUnit; exports Soundex index 1, LetterSet index 2; begin end. Библиотека импорта STRLIB приведена ниже и должна подклю- чаться в разделе uses программы, использующей функции Soundex и LetterSet: unit StrLib; interface function Soundex(S: String): String; function LetterSet(S: String): Longint; implementation function Soundex(S: String): String; external 'STRDLL* index 1; function LetterSet(S: String): Longint; external 'STRDLL* index 2; end. Следующая простая программа позволяет протестировать дина- мическую библиотеку STRDLL:
Гпава 9. Динамически подключаемые библиотеки 327 program TestDII; uses StrLib, WinCrt; const Vowels = (1 shl 0) + {‘A’} (1 shl 4) + {’E’} (1 shl 8) + {T} (1 shl 14) + {’O’} (1 shl 20) + {’U’} (1 shl 24); {’Y’} var S1, S2: String; begin WritefEnter string #1: ’); ReadLn(S1); WritefEnter string #2: ’); ReadLn(S2); WriteLnfSoundex for string #1: *, Soundex(SI)); WriteLnfSoundex for string #2: Soundex(S2)); if Soundex(SI) = Soundex(S2) then WriteLnfHm-m, strings are seemed to be equal’) else WriteLnfOh! strings are certainly different’); if (LetterSet(SI) and Vowels) <> 0 then WriteLnfString #1 contains vowels’) else WriteLnfString #1 does not contain vowels'); if (LetterSet(S2) and Vowels) <> 0 then WriteLnfString #2 contains vowels’) else WriteLnfString #2 does not contain vowels’); end. 9.5. ИНИЦИАЛИЗАЦИЯ И ЗАВЕРШЕНИЕ РАБОТЫ DLL-БИБЛИОТЕК Каждая динамическая библиотека может иметь код инициализации и завершения. Код инициализации вызывается системой Windows только однажды, при первой загрузке DLL-библиотеки в память. Например, если не- сколько приложений вызывают функцию LoadLibrar<y для загрузки динамической библиотеки STRDLL, то код ее инициализации выпол- няется только при первом вызове LoadLibrary, когда значение счетчи- ка привязок переходит из 0 в 1. Код инициализации должен поме- щаться в главный операторный блок begin ... end динамической библиотеки. Код завершения вызывается системой Windows перед тем, как ди- намическая библиотека физически выгружается из памяти, т.е. когда
328 Программирование в среде Borland Pascal для Windows счетчик привязок становится равным нулю. Если DLL-библиотека загружается после того, как она была полностью выгружена, то снова вызывается код ее инициализации. Код завершения должен быть оформлен в виде процедуры, которая инсталлируется при инициали- зации DLL-библиотеки как процедура выхода. Процедура выхода должна удовлетворять следующим требованиям: • не иметь параметров; • использовать дальнюю модель вызова; • не контролировать переполнение стека (директива $S-). В модуле SYSTEM объявлен указатель ExitProc, который указыва- ет на первую процедуру в цепочке процедур выхода. Правильная ин- сталляция процедуры завершения предполагает сохранение значения указателя ExitProc в некоторой глобальной переменной и его уста- новку на собственную процедуру выхода. Процедура выхода должна первым делом восстанавливать старое значение указателя ExitProc, чтобы после ее работы могли быть вызваны другие процедуры выхо- да, инсталлированные другими модулями, в том числе и библиотекой времени выполнения (модулем SYSTEM). Процедуры выхода вызы- ваются в порядке, обратном тому, в котором они были инсталлирова- ны (последняя инсталлированная процедура вызывается первой). Пе- ременная ExitCode при вызове процедуры выхода указывает причину завершения и равна: w ep_Free_DLL - при обычном завершении DLL-библиотеки; wep_System_Exit - при завершении, которое вызвано выходом из среды Windows. Ниже приводится пример DLL-библиотеки ENTREXIT, которая при инициализации и завершении сообщает об этом пользователю с помощью функции MessageBox: library EntrExit; uses WinTypes, WinProcs; const Caption = 'DLL Report'; var OldExitProc: Pointer; {$S-} { контроль переполнения стека должен быть отключен } procedure ExitDLL; far; { необходима дальняя модель вызова } begin ExitProc := OldExitProc; if ExitCode = wep_Free_DLL then MessageBox(0, 'DLL is unloading.', Caption, mb_Ok or mb_TaskModal); end; begin OldExitProc := ExitProc; ExitProc := @ExitDLL; MessageBox(0, 'DLL is loading.', Caption, mb_Ok or mb_TaskModal); end.
Гпава 9. Динамически подключаемые библиотеки 329 Программа EEDEMO (Entry-Exit Demo) динамически подключает DLL-библиотеку ENTREXIT: program EEDemo; uses WinTypes, WinProcs, WinCrt; const Caption = 'Application Execution Error*; var LibHandle: THandle; begin LibHandle := LoadLibrary('ENTREXIT.DLL'); if LibHandle < 32 then begin MessageBox(0, 'Cannot load Dynamic Link Library.', Caption, mb_Ok or mb_TaskModal or mbJconExclamation); Halt(255); end;* WriteLnfPress Enter to unload DLL...'); ReadLn; FreeLibrary(LibHandle); end. Обратите внимание на то, что динамическая библиотека ENTREXIT подключается в программе EEDEMO динамически. Если вы подключите ENTREXIT статически, то, скорее всего, потерпите неудачу. Дело в том, что при вызове кода инициализации использует- ся стек • того исполняемого модуля, который загружает DLL- библиотеку, а при завершении - того, который ее выгружает. При динамическом импорте для этих целей используется стек прикладной программы EEDEMO. В случае же статического импорта в качестве стека может использоваться стек модуля KERNEL (ядра системы Windows). Стек этого модуля очень мал и может оказаться недоста- точным, еслй в теле инициализации или в процедуре завершения вы- зываются функции, требующие большого числа параметров (которые, возможно, вызывают другие функции, тоже требующие множества параметров и т.д.). 9.6. ОГРАНИЧЕНИЯ ПРИ РАЗРАБОТКЕ DLL-БИБЛИОТЕК Существуют определенные требования и ограничения, которым должны удовлетворять динамически подключаемые библиотеки. Пе- речислим их: • DLL-библиотеки не могут экспортировать данные, а только функции и процедуры; • DLL-библиотеки имеют сегмент данных, но не имеют сегмента стека;
330 Программирование в среде Borland Pascal для Windows • динамическая библиотека пользуется стеком вызывающей при- кладной программы. Как следствие, значение сегментного регистра DS внутри кода DLL-библиотеки не равно значению сегментного регистра SS; ® если DLL-библиотека распределяет блоки глобальной памяти, то владельцем этих блоков обычно считается программа, которая вызва- ла функцию динамической библиотеки. Если программа завершается, ее блоки памяти автоматически освобождаются, поэтому динамиче- ская библиотека не вправе рассчитывать на то, что память, которую она выделила, будет доступна при следующих вызовах функций DLL. Сказанное не относится к динамическим библиотекам, разрабатывае- мым на Borland Pascal. Для того чтобы блоки памяти, выделенные с помощью New или GetMem принадлежали DLL-библиотеке, а не вы- зывающей программе, код инициализации динамической библиотеки устанавливает переменную HeapAllocFlags в значение gmem_Moveable + gmem.DDEShare. Эта переменная объявлена в модуле SYSTEM и используется менеджером памяти при обращении к функциям Windows; • если в DLL-библиотеке происходит ошибка времени выполнения (run-time error), то Windows завершает приложение, которое к ней обратилось. При этом код завершения программы не исполняется, что может стать причиной побочных ошибок. Поэтому динамическая библиотека должна быть защищена от ошибок времени выполнения; • если предусматривается взаимодействие DLL-библиотеки с вы- зывающей программой, то лучший способ сделать это - послать про- грамме сообщение с помощью функции PostMessage или SendMessage либо вызвать callback-функцию, адрес которой программа должна предварительно сообщить динамической библиотеке.
10 ОБМЕН ДАННЫМИ МЕЖДУ ПРИЛОЖЕНИЯМИ 10.1. ТЕХНОЛОГИЯ “ПЕРЕТЯНИ И ОСТАВЬ” 10.1.1. ВИД СО СТОРОНЫ Одним из наиболее впечатляющих эффектов Windows является воз- можность “перетягивания” мышью имен файлов из окна одной про- граммы в окно другой. Корректно реализовать такую операцию дос- таточно сложно, поэтому был разработан специальный механизм, получивший название “перетяни и оставь” (drag-and-drop). Он позво- ляет, в частности, находясь в File Manager, “схватить” мышью отме- ченный файл или группу файлов, при этом курсор изменится и станет похож на лист или стопку бумаг. Удерживая кнопку мыши нажатой и перемещая курсор по экрану, вы обнаружите, что в некоторых окнах курсор превращается в перечеркнутый кружок. Это говорит о том, что такие окна “не умеют” принимать файлы. В окнах, которые под- держивают технологию “drag-and-drop”, курсор имеет вид листа бу- маги (или стопки бумаг, если вы перетягиваете несколько файлов). Чтобы “оставить” файлы в окне, следует отпустить кнопку мыши. Поддержка механизма “drag-and-drop” в настоящее время является необходимым свойством любой хорошей прикладной программы. Механизм перетягивания файлов почти полностью реализован в Windows и скрыт от программиста. Последний должен позаботиться лишь о том, чтобы принять в программе перетянутые в окно файлы. Покажем, как это делается. 10.1.2. ПОДГОТОВКА К ПРИЕМУ ФАЙЛОВ Функции поддержки механизма “перетяни и оставь” находятся в мо- дуле SHELLAPI, который подключается к программе оператором uses. Прежде всего принимающее окно нужно зарегистрировать в Windows с помощью процедуры DragAcceptFiles(Wnd: HWnd; Accept: Bool);
332 Программирование в среде Borland Pascal для Windows где Wnd - дескриптор окна, которому разрешается или запрещается принимать файлы; Accept - булевский параметр, разрешающий (True) или запрещающий (False) прием. Как правило, процедура DragAcceptFiles вызывается с параметром True при создании окна и с параметром False при разрушении окна. Поэтому при объявлении оконного объекта TMainWindow следует перекрыть два метода: SetupWindow и WMDestroy. В следующем примере метод SetupWindow запускает работу механизма “перетяни и оставь” для главного окна программы, а метод WMDestroy - выклю- чает его: procedure TMainWindow.SetupWindow; begin Inherited SetupWindow; DragAcceptFiles(HWindow, True); end; procedure TMainWindow.WMDestroy(var Msg: TMessage); begin DragAcceptFiles(HWindow, False); inherited WMDestroy(Msg); end; 10.1.3. ПРИЕМ ФАЙЛОВ В момент “отпускания” пользователем файлов Windows посылает окну-приемнику сообщение wmJDropFiles; это происходит только в том случае, если окно поддерживает технологию “drag-and-drop”, т.е. зарегистрировано процедурой DragAcceptFiles. В параметре WParam сообщения wmJDropFiles программе передается дескриптор внутрен- ней структуры данных Windows, содержащей имена перетянутых фай- лов. Этот дескриптор актуален только в обработчике сообщения wmJDropFiles. Для доступа к именам файлов используется функция DragQueryFile, которая по номеру файла копирует в предоставленный программой буфер его полное имя, включая маршрут: DragQueryFile(Drop: THandle; Fileindex: Word; FileName: PChar; cb: Word): Word; где Drop - дескриптор внутренней структуры данных, содержащей имена перетянутых файлов; Fileindex - номер запрашиваемого файла, начиная с нуля; FileName - указатель на приемный буфер для имени файла; cb - размер буфера. У функции DragQueryFile существуют некоторые дополнительные возможности. Например, если параметр Fileindex равен SFFFF, функ- ция возвращает общее число перетянутых файлов. Отметим также,
Глава 10. Обмен данными между приложениями 333 что при передаче в параметре FileName значения nil, а в Fileindex - номера файла функция DragQueryFile возвращает размер буфера, требуемый для приема имени файла. Прием файлов завершается вызовом процедуры DragFinish, кото- рая освобождает зарезервированную для них системную память: DragFinish(Drop: THandle); где Drop - дескриптор списка перетянутых файлов. Приведем пример реализации метода обработки сообщения wm_DropFiles*B некотором оконном объекте TMainWindow: procedure TMainWindow.WMDropFiles(var Msg: TMessage); var HDrop: THandle; FileCount, I: Word; FileName: array [O..fsPathName] of Char; begin Show(swShowNormal); HDrop := Msg.WParam; FileCount := DragQueryFile(HDrop, Word(-1), nil, 0); for I := 0 to FileCount -1 do begin DragQueryFile(HDrop, I, FileName, SizeOf(FileName)); DropOne(FileName); end; DragFinish(HDrop); end; Сначала обработчик WMDropFiles активизирует и показывает ок- но, вызывая у объекта метод Show с параметром sw_ShowNormal. Необходимость этого действия связана с тем, что окно может прини- мать имена файлов даже тогда, когда оно сжато в пиктограмму. Далее выясняется общее число файлов (FileCount), а затем в цикле осуществ- ляется прием каждого файла с вызовом некоторого гипотетического метода DropOne для выполнения над ним конкретного действия, зави- сящего от назначения объекта TMainWindow. Вызов процедуры DragFinish завершает прием файлов. Программа DRAGDROP показывает работу механизма “drag-and- drop” на примере перетягивания файлов в многооконный текстовый редактор. При изучении этой программы обратите внимание на реа- лизацию в объекте TMainWindow метода DropOne: program DragDrop; {$R DRAGDROP.RES} uses WinTypes, WinProcs {$ifdefVer80}, Messages {$else}t Win31 {$endif}t WinDos, Strings, OWindows, OStdDIgs, OStdWnds, ShellAPI, BWCC; const idmMenu = 100;
334 Программирование в среде Borland Pascal для Windows cm_Help =101; type PMainWindow = ATMainWindow; TMainWindow = object(TM DI Window) constructor lnit(ATitle: PChar); procedure SetupWindow; virtual; procedure WMDestroy(var Msg: TMessage); virtual wm_First + wm_Destroy; procedure WMDropFiles(var Msg: TMessage); virtual wm__First + wm_DropFiles; procedure DropOne(FileName: PChar); procedure CMMDIFileNew(var Msg: TMessage); virtual cm__First + cm_MDIFileNew; procedure CMMDIFileOpen(var Msg: TMessage); virtual cm_First + cm__M DI FileOpen; procedure CMHelp(var Msg: TMessage); virtual cm_First + cm_Help; end; TDragDropApp = object(TApplication) procedure InitMainWindow; virtual; end; { Конструирует оконный объект и запоминает позицию меню Window} constructor TMainWindow.lnit(ATitle: PChar); begin inherited lnit(ATitle, LoadMenu(Hlnstance, PChar(idm_Menu))); ChildMenuPos := 3; end; {После создания окна включает для него механизм "перетяни и оставь”} procedure TMainWindow.SetupWindow; begin Inherited SetupWindow; DragAcceptFiles(HWindow, True); end; { Отключает механизм "перетяни и оставь" при уничтожении окна } procedure TMainWindow.WMDestroy(var Msg: TMessage); begin DragAcceptFiles(HWindow, False); inherited WMDestroy(Msg); end; { Принимает перетянутые файлы, открывая их в окнах редактора } procedure TMainWindow.WMDropFiles(var Msg: TMessage); var HDrop: THandle; FileCount, I: Word; FileName: array [0..fsPathName] of Char; begin HDrop := Msg.WParam; FileCount := DragQueryFile(HDrop, Word(-1), nil, 0); for I := 0 to FileCount -1 do begin
Гпава 10. Обмен данными между приложениями 335 DragQueryFile(HDrop, I, FileName, SizeOf(FileName)); DropOne(FileName); end; DragFinish(HDrop); Show(sw_ShowNormal); end; { Открывает заданный файл в дочернем окне редактора } procedure TMainWindow.DropOne(FileName: PChar); begin ApplicationA.MakeWindow(New(PFileWindow, lnit(@Self,", FileName))); end; { Создает новое пустое окно редактора по команде меню File-New} procedure TMainWindow.CMMDIFileNew(var Msg: TMessage); begin ApplicationA.MakeWindow(New(PFileWindow,lnit(@Self,","))); end; { Загружает файл в новое окно редактора по команде меню File-Open } procedure TMainWindow.CMMDIFileOpen(var Msg: TMessage); var FileName: array [O..fsPathNamd] of Char; begin if ApplicationA.ExecDialog(New(PFileDialog, Init(@Self, PChar(sd_FileOpen), StrCopy(FileName, '*.*')))) = id_Ok then ApplicationA.MakeWindow(New(PFileWindow, lnit(@Self,", FileName))); end; { Выдает подсказку по команде меню Help } procedure TMainWindow.CMHelp(var Msg: TMessage); begin MessageBox(HWindow, 'Drop files from File Manager' + ' into this window to open them.', 'Help', mb_Ok); end; { Создает объект главного окна программы } procedure TDragDropApp.lnitMainWindow; begin MainWindow := New(PMainWindow, InitfEditor with Drag-and-Drop support')); end; var DragDropApp: TDragDropApp; begin DragDropApp. Init('DragDrop'); DragDropApp. Run; DragDropApp.Done; e end. 10.1.4. ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ В библиотеке SHELLAPI существуют еще две функции поддержки механизма “перетяни и оставь”, которые могут оказаться очень по-
336 Программирование в среде Borland Pascal для Windows лезными, особенно если файлы представляются на экране пиктограм- мами. Функция DragQueryPoint выясняет, в какой точке окна находился курсор мыши в момент отпускания перетянутого файла или группы файлов: DragQueryPoint(Drop: THandle; var Pt: TPoint): Bool; где Drop - дескриптор внутренней структуры данных, содержащей имена перетянутых файлов; Pt - оконные координаты курсора мыши, которые возвращаются вызывающей программе. Возвращение функцией DragQueryPoint ненулевого значения озна- чает, что в момент отпускания мыши курсор находился внутри облас- ти клиента, а возвращение нуля, - вне этой области. Функцию DragQueryPoint разрешается вызывать только при обработке сообще- ния wmJDropFiles. Функция Extracticon извлекает из выполняемого ЕХЕ-файла, ди- намической DLL-библиотеки или ICO-файла пиктограмму по ее но- меру в файле: Extractlcon(lnst: THandle; ExeFileName: PChar; Iconindex: Word): HIcon; где Inst - дескриптор экземпляра прикладной программы, вызываю- щей функцию; ExeFileName - путь к файлу, в котором находится пик- тограмма; Iconindex - номер пиктограммы в файле (первой считается пиктограмма с нулевым индексом). При успешной загрузке пиктограммы функция возвращает ее деск- риптор. Если результат функции равен 0, то файл не содержит пикто- грамм, а если -1, то указан неверный путь к файлу. Чтобы узнать об- щее число пиктограмм в файле, нужно в параметре Iconindex передать значение $FFFF. 10.2. ПЕРЕДАЧА ДАННЫХ ЧЕРЕЗ БУФЕР ОБМЕНА 10.2.1. НАЗНАЧЕНИЕ БУФЕРА ОБМЕНА Буфер обмена (clipboard), или “карман”, представляет собой специ- альное средство Windows для обмена произвольными данными между прикладными программами. Приложения, которые поддерживают работу с буфером обмена, имеют в своем главном меню пункт Edit со стандартным набором команд для обмена редактируемыми данными. Работа с буфером обмена происходит по следующей схеме. Снача- ла одно приложение копирует в него данные. Это действие, как пра- вило, выполняется при выборе в меню команды Edit-Copy. При этом в буфере обмена создается полная копия данных. После этого другое
Гпава 10. Обмен данными между приложениями 337 приложение может скопировать себе данные из буфера обмена. Эта операция обычно связывается с выбором в меню команды Edit-Paste. Таким образом, буфер обмена выступает в качестве посредника, с помощью которого приложения могут обмениваться данными, даже не зная друг о друге. Для просмотра буфера обмена в Windows существует специальная утилита Clipboard, доступная из Program Manager. Однако, несмотря на название, это не сам буфер обмена, а только средство его просмот- ра (clipboard viewer). Буфер обмена всегда доступен в Windows; он не требует для своей работы запуска утилиты Clipboard, которой, вооб- ще говоря, может и не быть на диске. Возникает вопрос, - так что же представляет собой буфер обмена? Файл или зарезервированная область оперативной памяти? Ни то, ни другое. Это логическая область, работа с которой осуществляется посредством специальных функций Windows. 10.2.2. ОТКРЫТИЕ И ЗАКРЫТИЕ БУФЕРА ОБМЕНА Работа с буфером обмена всегда начинается с вызова функции OpenClipboard и завершается вызовом функции CloseClipboard. Функция OpenClipboard открывает буфер обмена для передачи или приема данных: OpenClipboard(Wnd: HWnd): Bool; где Wnd - дескриптор окна, с которым связывается буфер обмена. В случае успешного выполнения результат функции равен True. Если результат равен False, то буфер обмена открыт другим приложением и в данный момент недоступен. В этом случае вы можете или отказаться от обмена данных, или попытаться заново открыть буфер обмена через некоторое время. Вызывая функцию OpenClipboard, приложение блокирует буфер обмена от доступа со стороны других прикладных программ, поэтому его следует открывать только непосредственно перед записью или чтением данных и закрывать сразу после заверше- ния работы с ним. Вторая функция закрывает буфер обмена, после чего он становится доступен другим приложениям: CloseClipboard: Bool; 0 Результат равен True в случае успеха и False при возникновении ошибки. Открытие и закрытие буфера обмена можно сравнить с открытием и закрытием файла - также, как ввод и вывод выполняются только с открытым файлом, прием и передача данных выполняются только с открытым буфером обмена.
338 Программирование в среде Borland Pascal для Windows 10.2.3. ПЕРЕДАЧА ДАННЫХ Прежде чем передавать данные, нужно очистить буфер обмена и стать его владельцем. Для этого вызывается функция EmptyClipboard, ко- торая уничтожает в буфере обмена предыдущие данные, информирует об этом бывшего владельца с помощью сообщения wmJDestroyClipboard и назначает новым владельцем окно, открывшее буфер обмена. Формат функции: EmptyClipboard: Bool; Функция возвращает True при успешном выполнении и False в случае ошибки. Передачу данных выполняет функция SetClipboardData(Format: Word; Mem: THandle): THandle; где Format - двухбайтный идентификатор формата данных, Мет- дескриптор глобальной области памяти с передаваемыми данными. Функция SetClipboardData возвращает дескриптор переданных дан- ных или нуль, если происходит ошибка. Данные, которые помещаются в буфер обмена, должны принадле- жать к одному из известных системе Windows форматов. Идентифика- тором формата служит двухбайтное число. Наиболее часто исполь- зуемые форматы данных имеют зарезервированные идентификаторы (табл. 10.1). Табл. 10.1. Предопределенные форматы Clipboard Константа Описание cf_Text Данные представляют собой ASCII-текст, состоящий из символов кодовой таблицы ANSI. Каждая строка заверша- ется комбинацией управляющих символов “возврат каретки и перевод строки” - CR-LF. Весь текст оканчивается симво- лом с кодом нуль cfJBitmap Аппаратно-зависимое растровое изображение (возможно, совместимое с используемым в настоящее время устройст- вом отображения) cf_MetaFilePict Картинка, представленная как метафайл Windows. Структу- ра данных определяется записью TMetaFilePict cf_SYLK Данные в формате SYLK (SYmbolic LinK) cf_DIF Данные в формате DIF (Data Interchange Format), исполь- зуемом для обмена табличной информацией. В настоящее время активно поддерживается компанией Lotus Corporation cf_TIFF Данные в формате TIFF (Tag Image File Format). Использу- ется для обмена и хранения на диске растровых изображе- ний
Гпава 10. Обмен данными между приложениями 339 Окончание табл. 10.1 Константа Описание cf_OEMText То же, что и cf_Text, но символы используют кодовую таб- лицу OEM cf_DIB Аппаратно-независимое растровое изображение - область памяти, содержащая запись TBitmapInfo, за которой следу- ют данные битового образа cf_Palette Цветовая палитра Windows предоставляет программисту возможность определить и использовать свои собственные форматы данных. Регистрация нового формата выполняется с помощью функции RegisterClipboardFormat(FormatName: PChar): Word; где FormatName - строковый идентификатор нового формата. Функ- ция принимает название формата в виде ASCIIZ-строки и возвращает дескриптор, который может использоваться в процедурах работы с буфером обмена. Если формат с указанным в FormatName именем уже существует в Windows, функция RegisterClipboardFormat просто воз- вращает соответствующий ему числовой идентификатор, увеличивая счетчик регистраций формата. Если формат по каким-то причинам не может быть зарегистрирован, то возвращается значение 0. Отметим, что регистрация нового формата должна выполняться только один раз в самом начале программы. Чаще всего это делается в методе Initlnstance прикладного объекта (наследника TApplication). Передавая данные в буфер обмена, помните, что вы их не копируе- те, а отдаете. Нельзя отдать данные системе Windows или другому приложению, просто указав их адрес, так как в этом случае они по- прежнему будут принадлежать приложению-источнику, находясь в его памяти. Отдаваемые данные должны находиться в глобальной памяти Windows, распределение которой выполняется функцией GlobalAlloc(Flags: Word; Bytes: Longint): THandle; где Flags - способ выделения памяти (определяется комбинацией кон- стант из табл. 10.2); Bytes - размер требуемого участка памяти в бай- тах. Функция возвращает дескриптор выделенного Облока или нуль, если не удается удовлетворить запрос. Заметим кстати, что в программе следует избегать непосредствен- ных обращений к Windows за распределением памяти. Лучше исполь- зовать для этой цели библиотечные функции Borland Pascal (например, New, GetMem и др.), которые сами обращаются к Windows, но работают с памятью более экономно. Работа с буфером обмена составляет исключение из этого правила.
340 Программирование в среде Borland Pascal для Windows Блоки памяти, передаваемые в буфер обмена, могут быть переме- щаемыми (gmem_Moveable) или неперемещаемыми (gmem_Fixed), но не должны быть выгружаемыми (gmemJDiscardable). Предпочтение сле- дует отдавать перемещаемым блокам, так как они обеспечивают воз- можность дефрагментации памяти и улучшают ее использование. Табл. 10.2. Флаги функции Global Alloc Флаг Описание gmem_Fixed Выделяет неперемещаемый участок памяти gmem_Moveable Выделяет перемещаемый участок памяти gmem_NoCompact Запрещает при поиске свободного пространства дефрагментацию памяти gmem_No Discard Запрещает при выделении памяти выталкивать на диск выгружаемые блоки gmem_ZeroInit Инициализирует выделенный блок нулем gmem_Discardable Распределяет выгружаемый участок памяти gmem_Share, Распределяет разделяемый (между программами) gmem_DDEShare участок памяти То же, что gmem_Share gmem_Notify Посылает уведомление при каждой выгрузке блока данных на диск GHnd Равно gmem_Moveable or gmem_ZeroInit GPtr Равно gmemJFixed or gmem_ZeroInit Приведем пример выделения блока памяти для передачи в буфер обмена: Handle := GlobalAlloc(gmem_Moveable, Size); Полученный дескриптор передается в функцию SetClipboardData по- сле того, как блок заполнен данными. Очевидно, что дескриптор не может использоваться для прямого доступа к данным. Поэтому в Windows существует функция GlobalLock, которая по дескриптору глобального блока возвращает его адрес: Ptr := GlobalLock(Handle); Функция GlobalLock фиксирует в памяти глобальный блок и воз- вращает указатель на его начало. С помощью указателя можно осу- ществлять прямой доступ к данным в этом блоке. Для примера скопи- руем в него строку текста: StrCopy(Ptr, ’Some text');
Гпава 10. Обмен данными между приложениями 341 Перед вызовом функции SetClipboardData блок данных должен быть обязательно разблокирован функцией GlobalUnlock: GlobalUnlock(Handle); Теперь можно передавать данные в буфер обмена. Если они пред- ставляют собой строку текста, то при вызове функции SetClipboardData в параметре формата следует указать cf_Text: SetClipboardData(cf_Text, Handle); Запомните, что после обращения к SetClipboardData данные в гло- бальном блоке переходят в собственность буфера обмена и становятся недоступны программе. Ниже природится функция CopyTextToClipboard, которая копиру- ет в буфер обмена произвольную строку текста. Функция возвращает True, если строка копируется успешно, и False, если происходит ошибка: function CopyTextToClipboard(Wnd: HWnd; Text: PChar):. Boolean; var Handle: THandle; Ptr: Pointer; begin CopyTextToClipboard := False; if Text <> nil then if OpenClipboard(Wnd) then begin Handle := GlobalAlloc(gmem_Moveable, StrLen(Text) + 1); Ptr := GlobalLock(Handle); if Ptr <> nil then begin StrCopy(Ptr, Text); GlobalUnlock(Handle); EmptyClipboard; SetClipboardData(cf_Text, Handle); CopyTextToClipboard := True; end; CloseClipboard; end; end; 10.2.4. ПРИЕМ ДАННЫХ Прием данных из буфера обмена выполняет функция GetClipboardData(Format: Word): THandle; где Format - идентификатор формата принимаемых данных. Если в буфере обмена существуют данные с указанным в параметре Format
342 Программирование в среде Borland Pascal для Windows форматом, функция возвращает дескриптор глобального блока, кото- рый их содержит. Если данных с таким форматом нет или буфер об- мена пуст, результат функции равен нулю. Вы не должны изменять или освобождать блок памяти, который возвращает GetClipboardData. Дело в том, что этот блок принадлежит буферу обмена и единственной допустимой операцией является копи- рование данных из этого блока в память прикладной программы. Следующая функция иллюстрирует прием из буфера обмена текста: function PasteTextFromClipboard(Wnd: HWnd): PChar; var Handle: Thandle; begin PasteTextFromClipboard := nil; if OpenClipboard(Wnd) then begin Handle := GetClipboardData(cf_Text); if Handle <> 0 then begin PasteTextFromClipboard := StrNew(GlobalLock(Handle)); GlobalUnlock(Handle); end; CloseClipboard; .. end; end; Если окно может принимать данные только одного формата, необ- ходимо указать этот формат при вызове GetClipboardData и проана- лизировать возвращаемое значение. Если оно не равно нулю, то мож- но блокировать данные в памяти и копировать их в свою программу. Другое дело, если окно оперирует с данными разных форматов. На- пример, в текстовом процессоре Microsoft Word for Windows можно вставить в документ и текст, и растровое изображение, и метафайл. Чтобы обеспечить правильный прием данных, необходимо знать их формат. Для этого существует функция IsClipboardFormatAvailable, которая проверяет, имеются ли в буфере обмена данные с заданным форматом: IsClipboardFormatAvailable(Format: Word): Bool; где Format - идентификатор формата. Результат выполнения равен True, если в буфере обмена существуют данные с указанным форма- том, и False в противном случае. В буфере обмена может одновременно находиться несколько бло- ков разного формата, что используется, в основном, для передачи одних и тех же данных, представленных разными способами. Общее число доступных форматов возвращает функция
Гпава 10. Обмен данными между приложениями 343 CountCiipboardFormats: Integer; Для перебора форматов используется функция EnumClipboardFormats(Format: Word): Word; Она возвращает очередной доступный формат, следующий за пере- данным в параметре Format. Если значение параметра равно нулю, возвращается первый доступный формат. 10.2.5. ПЕРЕДАЧА И ПРИЕМ РАСТРОВЫХ ИЗОБРАЖЕНИЙ При передаче через буфер обмена растрового изображения в роли дескриптора данных выступает дескриптор самого растрового изо- бражения. С одной стороны, это упрощает нашу задачу, так как не требует использования функций GlobalAlloc, GlobalLock, GlobalUnlock. Однако с другой стороны, то обстоятельство, что про- грамма должна передавать в буфер обмена не собственное растровое изображение, а его копию, требует от нас умения копировать растро- вые изображения. В Windows нет соответствующей функции, поэтому мы реализовали ее для вас: function CopyBitmap(Wnd: HWnd; SrcBmp: HBitmap): HBitmap; var DestBmp, OldSrcBmp, OldDestBmp: HBitmap; BitmapRec: TBitmap; WndDC, SrcDC, DestDC: HDC; begin { Если параметры имеют недопустимые значения, то возвращаем нуль } CopyBitmap := 0; if (Wnd = 0) or (SrcBmp = 0) then Exit; { Получаем контекст дисплея WndDC для заданного окна Wnd} WndDC := GetDC(Wnd); { Создаем два вспомогательных контекста дисплея: SrcDC и DestDC } SrcDC := CreateCompatibleDC(WndDC); DestDC := CreateCompatibleDC(WndDC); { Создаем пустое растровое изображение DestBmp, совместимое с контекстом дисплея WndDC } GetObject(SrcBmp, SizeOf(TBitmap), @BitmapRec); DestBmp := CreateCompatibleBitmap(WndDC, BitmapRec.bmWidth, BitmapRec.bmHeight); { Освобождаем уже не нужный контекст дисплея WndDC } ReleaseDC(Wnd, WndDC); { Выбираем в исходном контексте SrcDC растровое изображение SrcBmp, а в целевом контексте DestDC - растровое изображение DestBmp } OldSrcBmp := SelectObject(SrcDC, SrcBmp); OldDestBmp := SelectObject(DestDC, DestBmp); { Копируем изображение из исходного контекста SrcDC в целевой DestDC} BitBlt(DestDC, 0, 0, BitmapRec.bmWidth, BitmapRec.bmHeight, SrcDC, 0, 0, SrcCopy);
344 Программирование в среде Borland Pascal для Windows { Восстанавливаем в SrcDC и DestDC прежние растровые изображения } SelectObject(SrcDC, OldSrcBmp); SelectObject(DestDC, OldDestBmp); { Удаляем временные контексты дисплея SrcDC и DestDC} DeleteDC(SrcDC); DeleteDC(DestDC); { Возвращаем растровое изображение DestBmp как значение функции } CopyBitmap := DestBmp; end; Функция CopyBitmap имеет два параметра: Wnd - дескриптор ок- на, используемого для создания совместимой с ним копии растрового изображения, и SrcBmp - исходное растровое изображение. Результа- том функции является копия растрового изображения SrcBmp. Копирование основано на использовании функции BitBlt, которая переносит битовый образ с одной поверхности отображения на дру- гую. Исходная (SrcBmp) и результирующая (DestBmp) поверхности отображения задаются своими контекстами устройств SrcDC и DestDC, в которых они устанавливаются (выбираются) с помощью функции SelectObject. Обратите внимание на то, что контексты SrcDC, DestDC и результирующее растровое изображение DestBmp создают- ся совместимыми с контекстом отображения окна, передаваемого в параметре Wnd. Это обеспечивает создание такой копии растрового изображения, которую можно будет вывести в окно типа Wnd. На основе функции CopyBitmap очень просто реализовать проце- дуры копирования растрового изображения через буфер обмена. В приведенной ниже программе CLIPBMP эти процедуры являются методами оконного объекта TMainWindow: CLIPBMP.INC const idm_Menu = 100; ida_AccTable = 100; CLIPBMP.PAS program ClipBmp; {$R CLIPBMP.RES} uses WinTypes, WinProcs {$ifdef Ver80}, Messages {$endif}, OWindows; {$1 CLIPBMP.INC} type PMainWindow = ATMainWindow; TMainWindow = object(TWindow) Bitmap: HBitmap; { отображаемая в окне картинка } Width: Integer; { ширина картинки} Height: Integer; { высота картинки} constructor lnit(AParent: PWindowsObject; ATitle: PChar); destructor Done; virtual; procedure Cut; procedure Copy;
Гпава 10. Обмен данными между приложениями 345 procedure Paste; procedure Clear; procedure DeleteBitmap; procedure SetBitmap(ABitmap: HBitmap); procedure Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); virtual; procedure WMSize(var Msg: TMessage); virtual wm_First + wm_Size; procedure CMCut(var Msg: TMessage); virtual cm_First + cm_EditCut; procedure CMCopy(var Msg: TMessage); virtual cm_First + cmJEditCopy; procedure CMPaste(var Msg: TMessage); virtual cm_First + cmJEditPaste; procedure CMCIear(var Msg: TMessage); virtual cm_First + cmJEditClear; end; PCIipBmpApp = ATCIipBmpApp; TClipBmpApp = object(TApplication) procedure Initlnstance; virtual; procedure InitMainWindow; virtual; end; { Создает в памяти копию растрового изображения } function CopyBitmap(Wnd: HWnd; SrcBmp: HBitmap): HBitmap; var DestBmp, OldSrcBmp, OldDestBmp: HBitmap; BitmapRec: TBitmap; WndDC, SrcDC, DestDC: HDC; begin CopyBitmap := 0; If (Wnd = 0) or (SrcBmp = 0) then Exit; WndDC := GetDC(Wnd); SrcDC := CreateCompatibleDC(WndDC); DestDC := CreateCompatibleDC(WndDC); GetObject(SrcBmp, SizeOf(TBitmap), @BitmapRec); DestBmp := CreateCompatibleBitmap(WndDC, BitmapRec.bmWidth, BitmapRec.bmHeight); ReleaseDC(Wnd, WndDC); OldSrcBmp := SelectObject(SrcDC, SrcBmp); OldDestBmp := SelectObject(DestDC, DestBmp); BitBlt(DestDC, 0, 0, BitmapRec.bmWidth, BitmapRec.bmHeight, SrcDC, 0, 0, SrcCopy); SelectObject(SrcDC, OldSrcBmp); SelectObject(DestDC, OldDestBmp); „ DeleteDC(SrcDC); DeleteDC(DestDC); CopyBitmap := DestBmp; end; { Конструирует оконный объект и инициализирует его поля } constructor TMainWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle);
346 Программирование в среде Borland Pascal для Windows with Attr do begin Style := Style or ws__VScroll or ws_HScroll; Menu := LoadMenu(Hlnstance, PChar(idm_Menu)); end; Scroller := New(PScroller, lnit(@Self, 1,1,100, 100)); Bitmap := 0; Width := 0; Height := 0; end; { Удаляет картинку и разрушает оконный объект } destructor TMainWindow.Done; begin DeleteBitmap; inherited Done; end; { Переносит картинку Bitmap в буфер обмена, удаляя ее из окна } procedure TMainWindow.Cut; begin if Bitmap <> 0 then if OpenClipboard(HWindow) then begin EmptyClipboard; SetClipboardData(cf_Bitmap, Bitmap); CloseClipboard; Bitmap := 0; Width := 0; Height := 0; InvalidateRect(HWindow, nil, True); end; end; { Копирует картинку Bitmap в буфер обмена } procedure TMainWindow.Copy; var Handle: THandle; begin if Bitmap <> 0 then if OpenClipboard(HWindow) then begin Handle := CopyBitmap(HWindow, Bitmap); if Handle <> 0 then begin EmptyClipboard; SetClipboardData(cf_Bitmap, Handle); end; CloseClipboard; end; end; { Извлекает картинку из буфера обмена и помещает ее в окно } procedure TMainWindow.Paste;
Гпава 10. Обмен данными между приложениями 347 var Handle: THandle; begin if OpenClipboard(HWindow) then begin Handle := GetClipboardData(cf_Bitmap); if Handle <> 0 then begin SetBitmap(CopyBitmap(HWindow, Handle)); lnvalidateRect(HWindow, nil, True); end; CloseClipboard; end; end; { Очищает содержимое окна } procedure TMainWindow.Clear; begin DeleteBitmap; lnvalidateRect(HWindow, nil, True); end; { Удаляет картинку Bitmap } procedure TMainWindow.DeleteBitmap; begin if Bitmap <> 0 then begin DeleteObject(Bitmap); Bitmap := 0; end; Width := 0; Height := 0; end; { Устанавливает в объекте заданную картинку} procedure TMainWindow.SetBitmap(ABitmap: HBitmap); var BitmapRec: TBitmap; begin DeleteBitmap; if ABitmap <> 0 then begin GetObject(ABitmap, SizeOf(TBitmap), ©BitmapRec); Bitmap := ABitmap; Width := BitmapRec.bmWidth; Height := BitmapRec.bmHeight; end; end; { Отображает в окне картинку Bitmap } procedure TMainWindow.Paint(PaintDC: HDC; var Paintinfo: TPaintStruct); var MemDC: HDC; OldBitmap: HBitmap;
348 Программирование в среде Borland Pascal для Windows begin if Bitmap <> 0 then begin MemDC := CreateCompatibleDC(PaintDC); OldBitmap := SelectObject(MemDC, Bitmap); BitBlt(PaintDC, 0, 0, Width, Height, MemDC, 0, 0, SrcCopy); SelectObject(MemDC, OldBitmap); DeleteDC(MemDC); end; end; { Перехватывает сообщение wm_Size для установки диапазона скроллинга } procedure TMainWindow.WMSize(var Msg: TMessage); var R: TRect; begin inherited WMSize(Msg); GetClientRect(HWindow, R); with R do ScrollerA.SetRange(Width - (Right - Left), Height - (Bottom - Top)); end; { По команде Edit-Cut вырезает картинку и переносит ее в буфер обмена } procedure TMainWindow.CMCut(var Msg: TMessage); begin Cut; end; { По команде Edit-Сору копирует картинку Bitmap в буфер обмена } procedure TMainWindow.CMCopy(var Msg: TMessage); begin Copy; end; { По команде Edit-Paste извлекает картинку из буфера обмена } procedure TMainWindow.CMPaste(var Msg: TMessage); begin Paste; end; { По команде Edit-Clear очищает окно } procedure TMainWindow.CMCIear(var Msg: TMessage); begin Clear; end; { Загружает таблицу акселераторов при инициализации программы } procedure TClipBmpApp.lnitlnstance; begin inherited Initlnstance; HAccTable := LoadAccelerators(Hlnstance, PChar(ida_AccTable)); end; { Создает объект главного окна программы } procedure TClipBmpApp.lnitMainWindow; begin MainWindow := New(PMainWindow, lnit(nil, ’Clipboard Demo')); end;
Глава 10. Обмен данными между приложениями 349 var ClipBmpApp: TClipBmpApp; begin ClipBmpApp.lnit('ClipBmp'); ClipBmpApp.Run; ClipBmpApp. Done; end. Объект TMainWindow является наследником TWindow и содержит три дополнительных поля: дескриптор Bitmap, ширину Width и высо- ту Height растрового изображения. Эти элементы данных описывают растровое изображение, выводимое в методе Paint. По умолчанию дескриптор растрового изображения равен нулю и окно создается пустым. Чтобы поместить в окно картинку, скопируйте ее сначала в буфер обмена, например из программы Resource Workshop или Paint Brush. После этого переключитесь на приложение CLIPBMP и коман- дой Edit-Paste вставьте картинку из буфера обмена в окно. В CLIPBMP реализованы все стандартные команды меню Edit, которые выполняют следующие действия: Cut - “вырезает” из окна растровое изображение. В результате оно удаляется из окна и помещается в буфер обмена; Сору - копирует растровое изображение в буфер обмена; Paste - извлекает растровое изображение из буфера обмена. В ре- зультате в окне создается его копия; Clear - очищает содержимое окна. 10.3. ДИНАМИЧЕСКИЙ ОБМЕН ДАННЫМИ МЕЖДУ ПРИЛОЖЕНИЯМИ (DDE) 10.3.1. ОБЩИЕ ПОЛОЖЕНИЯ Особый интерес для программиста представляет стандарт динамиче- ского обмена данными между программами. Тема эта сложна как для описания, так и для понимания, поэтому рекомендуем читать эту часть главы тем, кто уже приобрел некоторый опыт создания прило- жений для Windows. DDE (от англ. Dynamic Data Exchange) - это протокол (стандарт) динамического обмена данными между Windows-программами. Обмен называется динамическим потому, что может происходить не по ини- циативе пользователя (для этого достаточно буфера обмена), а по инициативе программы в любой момент ее работы. Механизм DDE служит не только для передачи информации из одной программы в другую, он позволяет организовать совместное использование данных несколькими программами по принципу “клиент-сервер”. Подобно
350 Программирование в среде Borland Pascal для Windows тому, как DLL-библиотеки обеспечивают разделяемость кода, DDE- протокол обеспечивает разделяемость данных. При организации совместного использования данных возникают следующие проблемы: • чтобы программы, потребляющие данные, могли обнаружить и установить связь с программами, которые эти данные поставляют, необходима система имен; • данные должны быть разделяемыми. Windows изолирует про- граммы от взаимного влияния друг на друга, строго разграничивая занимаемую ими память. Очевидно, что передача некоторого блока данных между двумя программами предполагает выделение сегмента глобальной памяти, доступ к которому был бы возможен со стороны обеих программ; • доступ к данным должен быть упорядоченным. Так как сразу не- сколько программ могут читать и изменять данные, отсутствие согла- сованности в их действиях может привести к нарушению целостности данных. Поэтому нужен механизм синхронизации доступа. Одной из его задач должно быть своевременное уведомление программ о каж- дом изменении данных (это особенно важно в программах отображе- ния динамических процессов). DDE-протокол решает все перечисленные проблемы. Взаимодей- ствие программ через DDE-протокол строится по принципу “клиент- сервер”. В соответствии с этим принципом обмен данными всегда осуществляется между сервером - программой, которая владеет дан- ными, и клиентом - программой, которая их использует. Инициато- ром обмена является клиент, который устанавливает соединение (conversation) с сервером и таким образом получает возможность за- прашивать у сервера данные и услуги. Запрос данных или услуг назы- вается транзакцией (transaction). Сервер реагирует на транзакции клиента, посылая ему данные или выполняя услуги. Когда клиент больше не нуждается в услугах сервера, он разрывает соединение. DDE предусматривает, что у одного сервера может быть несколько клиентов, а клиенты могут обращаться за данными ко множеству серверов. Более того, одна и та же программа может одновременно выступать как в роли клиента, так и в роли сервера. Для доступа к данным сервера используется трехуровневая иерар- хическая система именования, которая включает имя службы (service name), имя темы (topic name) и имя элемента данных (item name). Все эти имена представляют собой нуль-терминированные строки. Имя службы - это идентификатор, по которому клиент отыскивает нужный сервер при установке с ним соединения. Каждый сервер мо- жет иметь одну или несколько служб, имена которых он сообщает Windows при запуске. Обычно один сервер поддерживает только одну службу, имя которой совпадает с именем его программы.
Гпава 10. Обмен данными между приложениями 351 Имя темы - это контекст данных в рамках службы. Для сервера, который хранит свои данные на диске, это может быть имя файла. Имя темы указывается вместе с именем службы при установке соеди- нения и передается серверу. Сервер проверяет, существует ли такая тема, и разрешает соединение. Имя элемента данных - это идентификатор информационной еди- ницы обмена между клиентом и сервером. Он сообщается клиентом непосредственно в момент выполнения транзакции и указывает, какие именно данные клиент желает получить или передать. Информацион- ной единицей может быть число, строка, запись, параграф текста, растровое изображение. Динамический обмен данными может происходить двумя разными способами: через'механизм сообщений и через функции библиотеки DDEML. Первый способ предполагает посылку и прием специальных DDE- сообщений вида wm_dde_XXXX и считается устаревшим. Второй способ основан на использовании динамически подклю- чаемой библиотеки DDEML (от англ. Dynamic Data Exchange Management Library), которая служит API-интерфейсом к DDE. Про- граммы, работающие через DDEML, полностью совместимы с про- граммами, работающими через DDE-сообщения, и могут обменивать- ся с ними данными. Этот способ мы и будем использовать. 10.3.2. ПОДКЛЮЧЕНИЕ К DDEML Чтобы программе стали доступны средства DDEML, нужно указать в операторе uses соответствующий модуль импорта: uses DDEML; В нем содержатся объявления всех необходимых для работы констант, типов и заголовков функций. Прежде чем обратиться к любой из функций DDEML, программа должна подключиться к этой библиотеке с помощью функции Ddelnitialize(var Inst: Longint; Callback: TCallback; Cmd, Res: Longint): Word; где Inst - идентификатор программы в библиотеке DDEML. Про- грамма должна сохранять дескриптор, возвращаемой в этом пара- метре, и передавать его во все остальные функции DDEML, требую- щие при вызове параметр Inst. Перед первым вызовом Ddelnitialize значение Inst должно быть равно 0. Если оно отличается от нуля, то происходит переинициализация DDEML. Callback - указатель на callback-функцию DDE, которая предназначена для обработки тран- закций, посылаемых программе (она будет рассмотрена позже); Cmd -
352 Программирование в среде Borland Pascal для Windows комбинация флагов из табл. 10.3, определяющих особенности DDE- программы и обрабатываемые ею транзакции. Этот параметр, в част- ности, позволяет указать, является программа DDE-сервером или DDE-клиентом. Res - зарезервирован, должен быть равен нулю. При успешном завершении функция возвращает dmlErr_No_Error (0). Табл. 10.3. Флаги функции Ddelnitialize Флаг Описание AppClass_Standard Стандартное DDE-приложение, выступающее в роли и клиента, и сервера AppClass_Monitor Приложение-монитор. Оно получает все собы- тия, связанные с работой DDEML, и может использоваться для отладки AppCmd_ClientOnly Приложение регистрируется только как DDE- клиент, что исключает посылку ему серверных транзакций (о транзакциях см. ниже) AppCmd_FilterInits Запрещает для сервера прием транзакций уста- новки соединения xtyp_Connect и xtyp_WildConnect до регистрации им службы. При первом вызове Ddelnitialize этот флаг мож- cbf_Fail_SelfConnections но не указывать Запрещает установку соединения “сам с собой” для программ, являющихся одновременно и клиентом, и сервером. Тем не менее установка соединения с другим экземпляром этой же про- граммы остается возможной cbf_Fail_Connections Запрещает получение транзакций xtyp_Connect и xtyp_WildConnect, а следовательно, установку соединений с данным сервером cbf_Fail_Advises Запрещает получение сервером транзакций xtyp_AdvStart и xtyp.AdvStop cbf_Fail_Executes Запрещает получение сервером транзакции xtyp_Execute cbf_Fail_Pokes Запрещает получение сервером транзакции xtyp_Poke, прием данных от клиентов закрыт cbf_Fail_Requests Запрещает получение сервером транзакции xtyp_Requests, а следовательно, запросов дан- ных от клиентов cbf_Fail_AHSvrXActions Запрещает получение всех серверных транзак- ций; является комбинацией флагов cbf.FaiLXXXX cbf_Skip_Connect_Confirms Запрещает для клиента прием уведомлений xtyp_Connect_Confirm об успешной установке соединения
Гпава 10. Обмен данными между приложениями 353 Окончание табл. 10.3 Флаг Описание cbf_Skip_Registrations Запрещает получение уведомлений xtyp_Register, посылаемых всем DDE- программам при регистрации новой службы cbf_Skip_Unregistrations Запрещает получение уведомлений xtyp_Unregister, посылаемых всем DDE- программам при удалении службы cbf_Skip_Discoilnects Запрещает получение уведомлений xtyp_Disconnect, посылаемых при разрыве со- единения cbf_Skip_AHNotifications Запрещает получение всех уведомлений; являет- ся комбинацией флагов cbf_Skip_XXXX В результате вызова Ddelnitialize программа регистрирует свою callback-функцию и получает уникальный дескриптор, отличающий ее от других программ, использующих DDEML. В конце работы программа должна сообщить библиотеке DDEML о своем завершении и освободить выделенные для нее ресурсы. Для этого вызывается функция DdeUninitialize(Inst: Longint): Bool; где Inst - это идентификатор программы в библиотеке DDEML, полу- ченный ранее с помощью Ddelnitialize. Результат функции равен True при нормальном завершении. При работе с DDEML (между вызовами Ddelnitialize и DdeUninitialize) могут возникать различные ошибки. Причину по- следней ошибки можно выяснить с помощью функции DdeGetLastError(Inst: Longint): Word; где Inst - идентификатор программы в библиотеке DDEML. Функция возвращает код ошибки (см. справочник по Borland Pascal), а саму ошибку сбрасывает. Если ошибки нет, возвращается значение dmIErr_No_Error. 10.3.3. УПРАВЛЕНИЕ ИМЕНАМИ Имена служб, тем и элементов данных, используемые при взаимодей- ствии клиента и сервера, являются глобальными объектами, для управления которыми в DDEML существует специальное подмноже- ство функций. Самые важные из них - функции создания, удаления и сравнения имен. Для создания имени вызывается функция 12 Зак. 1049
354 Программирование в среде Borland Pascal для Windows DdeCreateStringHandle(Inst: Longint; PSz: PChar; CodePage: Integer): HSz; где Inst - идентификатор программы в библиотеке DDEML; PSz - указатель на нуль-терминированную строку, содержащую имя; CodePage - кодовая таблица для символов строки PSz (обычно cp_WinAnsi). Функция DdeCreateStringHandle регистрирует в Windows строку PSz и возвращает ее дескриптор. Если такая строка уже зарегистриро- вана (при сравнении прописные и строчные буквы не различаются), то копия строки не создается, а увеличивается счетчик ее использования и возвращается уже существующий дескриптор. Дескрипторы имен имеют тип HSz, определенный через тип Longint. Для удаления имени вызывается функция DdeFreeStringHandle(Inst: Longint; HSz: HSz): Bool; где Inst - идентификатор программы в библиотеке DDEML; HSz - дескриптор строки, полученный в результате вызова DdeCreateStringHandle. Функция DdeFreeStringHandle уменьшает счетчик использования строки и удаляет ее только в том случае, если он становится равен нулю. При успешном завершении функция воз- вращает True. Для сравнения имен служит функция DdeCmpStringHandles(HSzl, HSz2: HSz): Integer; где HSzl и HSz2 - сравниваемые дескрипторы строк. Функция воз- вращает значение: -1, если HSzl равен нулю или меньше HSz2; 0, - если HSzl и HSz2 равны (в том числе равны нулю); 1, - если HSz2 ра- вен нулю или меньше HSzl. Функции управления именами являются вспомогательными, с их применением вы познакомитесь ниже. 10.3.4. УПРАВЛЕНИЕ БЛОКАМИ ДАННЫХ Так как передача данных между приложениями осуществляется через глобальную память, DDEML предоставляет набор функций, предна- значенных для управления глобальными блоками данных. Дескрип- торы этих блоков имеют тип HDDEData, определенный через тип Longint. Для создания глобального блока DDE-данных служит функция DdeCreateDataHandle(Inst: Longint; Src: Pointer; Cb, Off: Longint; Item: HSz; Fmt, Cmd: Word): HDDEData; где Inst - идентификатор программы в библиотеке DDEML; Src - указатель на буфер с исходными данными (если он равен nil, глобаль-
Гпава 10. Обмен данными между приложениями 355 ный блок выделяется, но не инициализируется); СЬ - размер блока данных в байтах (если он равен нулю, то значение Src игнорируется); Off - смещение в буфере Src, начиная с которого исходные данные копируются в глобальную память; Item - имя элемента данных, полу- ченное с помощью DdeCreateStringHandle; Fmt - формат данных (см. табл. lO.l); Cmd-флаг создания (0 или hdata_AppOwned). Функция выделяет блок глобальной памяти, копирует в него данные из буфера Src и возвращает дескриптор, который может пере- даваться между программами во время транзакций. Если при обраще- нии к функции DdeCreateDataHandle в параметре Cmd указан флаг hdata_AppOwned, то владельцем созданного блока считается при- кладная программа, которая и должна его удалять. Если параметр Cmd равен нулю, то после передачи дескриптора данных в другую функцию библиотеки DDEML он становится недействительным и ответственность за его освобождение возлагается на Windows. Флаг hdata_AppOwned употребляется только в тех случаях, когда данные создаются по инициативе прикладной программы. Если же блок дан- ных создается по запросу и возвращается в библиотеку DDEML, в Cmd передается 0. Для доступа к содержимому блока данных предназначена функция DdeAccessData(Data: HDDEData; DataSize: PLongint): Pointer; где: Data - дескриптор глобального блока данных; DataSize - адрес переменной, в которой возвращается размер данных. Функция закре- пляет в памяти глобальный блок и возвращает указатель, который может использоваться для чтения данных (но не для записи!). После чтения блок должен быть “раскреплен” с помощью функ- ции DdeUnaccessData(Data: HDDEData): Bool; где Data - дескриптор данных. Функция возвращает True в случае успеха. В DDEML существует полезная функция, которая позволяет ско- пировать данные из глобального блока в локальный буфер приклад- ной программы: DdeGetData(Data: HDDEData; Dst: Pointer; Max, Off: Longint): Longint; * где Data - дескриптор данных; Dst - указатель на локальный буфер, в который копируются данные (если он равен nil, то функция возвраща- ет размер глобального блока в байтах); Мах - размер зарезервиро- ванного буфера; Off - смещение в глобальном блоке, начиная с кото- рого данные копируются в буфер. Функция возвращает количество скопированных байт.
356 Программирование в среде Borland Pascal для Windows Удаление принадлежащих программе DDE-данных выполняется с помощью функции DdeFreeDataHandle(Data: HDDEData): Bool; где Data - дескриптор DDE-данных. Функция возвращает значение True, если данные удалены успешно, и False в случае ошибки. Про- грамма обязана вызывать DdeFreeDataHandle только для тех блоков данных, которые создавались с флагом hdata_AppOwned (см. выше). 10.3.5. РЕГИСТРАЦИЯ СЛУЖБЫ И УСТАНОВКА СОЕДИНЕНИЯ После подключения к DDEML клиент и сервер ведут себя по-разному. Рассмотрим наиболее типичное их поведение. Программа-сервер регистрирует имя службы, вызывая функцию DdeNameService(Inst: Longint; HSzl, HSz2: HSz; Cmd: Word): HDDEData; где Inst - идентификатор программы в библиотеке DDEML; HSzl - имя службы, полученное с помощью функции DdeCreateStringHandle; HSz2 - зарезервирован и должен быть равен нулю; Cmd- флаги, регу- лирующие работу функции (табл. 10.4). При успешном выполнении функция возвращает ненулевое значение. Табл. 10.4. Флаги функции DdeNameService Флаг Описание dns_Register Регистрирует новую службу dns_Unregister Удаляет существующую службу. Если дескриптор HSzl равен нулю, то удаляются все службы dns.FilterOn Запрещает для сервера прием транзакций xtyp_Connect (установку соединения), если имя службы не совпадает с регистрируемым. Этот флаг принимается по умолчанию dns_FilterOfT Разрешает для сервера прием транзакций xtyp_Connect, в которых имя службы не совпадает с регистрируемым С помощью DdeNameService сервер извещает Windows о своем су- ществовании и становится доступным для программ-клиентов, же- лающих установить с ним соединение. Соединение всегда устанавливается по инициативе клиента, кото- рый вызывает функцию DdeConnect(Inst: Longint; Service, Topic: HSz; CC: PConvContext): HConv; где Inst - идентификатор программы в библиотеке DDEML; Service, Topic - имена службы и темы, полученные с помощью функции
Гпава 10. Обмен данными между приложениями 357 DdeCreateStringHandle; СС - указатель на запись, содержащую кон- текст соединения (код страны, номер кодовой таблицы, код языка и др.). Если параметр Service равен нулю, соединение устанавливается с первым попавшимся сервером, поддерживающим тему Topic. Если значение Topic равно нулю, то тема соединения выбирается произ- вольно. В параметре СС обычно передается значение nil, что соответ- ствует контексту по умолчанию. Функция возвращает дескриптор установленного соединения или нуль, если соединение не может быть установлено. 10.3.6. ТРАНЗАКЦИИ После благополучной установки соединения клиент получает право посылать серверу транзакции, запрашивая или передавая ему данные, а также запрашивая услуги. Существует два режима исполнения тран- закций: синхронный и асинхронный. Синхронная транзакция выполняется как обычный вызов функции. Начиная синхронную транзакцию, клиент указывает максимальное количество времени (тайм-аут), в течение которого он согласен ожи- дать ее завершения. Во время синхронной транзакции библиотека DDEML входит в цикл обработки сообщений и не возвращает клиен- ту управления до завершения транзакции или до наступления тайм- аута. Если за отведенное время сервер не успевает выполнить тран- закцию или происходит какая-нибудь ошибка, транзакция завершает- ся и в приложение возвращается значение, свидетельствующее о не- удаче. При успешном выполнении транзакции возвращается ее ре- зультат, например, дескриптор блока данных, если клиент запраши- вал у сервера данные. В каждый момент времени клиент может запро- сить только одну синхронную транзакцию. Асинхронная транзакция обрабатывается иначе. После начала асинхронной транзакции управление сразу возвращается клиенту, чтобы он мог продолжить свою работу. Когда сервер завершает тран- закцию, клиенту посылается об этом уведомление через callback- функцию, регистрируемую при вызове Ddelnitialize. Одновременно клиент может запросить несколько асинхронных транзакций, поэтому каждой из них назначается уникальный идентификатор асинхронной транзакции типа Longint. Он сообщается клиенту при начале транзак- ции и передается в его callback-функцию, когда транзакция заверша- ется. По этому идентификатору клиент может судить о том, какая транзакция завершилась. Выполнение синхронных и асинхронных транзакций можно срав- нить с посылкой сообщений функциями SendMessage и PostMessage. Функция SendMessage возвращает управление только после заверше- ния обработки сообщения, что соответствует посылке синхронной транзакции. Функция PostMessage просто помещает сообщение в оче-
358 Программирование в среде Borland Pascal для Windows редь и немедленно возвращает управление программе, что соответст- вует посылке асинхронной транзакции. Основной характеристикой транзакции является ее тип. Он указы- вается клиентом при начале транзакции и передается в callback- функцию сервера. В зависимости от типа транзакции сервер выполня- ет те или иные действия, например возвращает клиенту данные или исполняет его команду. Чтобы послать серверу транзакцию, клиент вызывает функцию DdeClientTransaction(Data: Pointer; DataLen: Longint; Conv: HConv; Item: HSz; Fmt, DataType: Word; Timeout: Longint; Result: PLongint): HDDEData; где Data - указатель на буфер с данными, передаваемыми серверу; DataLen - размер буфера. Если значение DataLen равно -1, то в Data должен помещаться не указатель, а дескриптор глобального блока данных типа HDDEData, полученный с помощью функции DdeCreateDataHandle. Если в транзакции данные не передаются, то в Data передается nil, а в DataLen - 0. Conv - дескриптор соединения, в рамках которого выполняется транзакция; Item - имя элемента дан- ных; Fmt - формат передаваемых или принимаемых данных (см. табл. 10.1); DataType - тип транзакции (табл. 10.5); Timeout - тайм-аут в миллисекундах для синхронной транзакции или Timeout_Async (-1) для асинхронной (асинхронную транзакцию мож- но прервать с помощью функции DdeAbandonTransaction - см. спра- вочник по Borland Pascal); Result - указатель на переменную типа Longint, в которую помещается результат транзакции (для асинхрон- ных транзакций в эту переменную помещается уникальный идентифи- катор транзакции). Если программу не интересует результат транзак- ции, в параметре Result передается nil (обычное значение для син- хронных транзакций). Для синхронной транзакции, в которой у сервера запрашиваются данные, DdeClientTransaction возвращает дескриптор данных. Для остальных синхронных и асинхронных транзакций функция возвра- щает True при успешном выполнении и False в случае ошибки (например, занятости сервера). Причину ошибки можно уточнить с помощью функции DdeGetLastError. Среди транзакций клиента особо выделим xtyp_AdvStart и xtyp_AdvStop. С помощью транзакции xtyp_AdvStart клиент может установить связь с требуемым элементом данных сервера, а с помо- щью транзакции xtyp_AdvStop прервать ее. После установки связи сервер начинает посылать клиенту уведомления о каждом изменении указанного элемента данных. Эти посылки называют циклом уведом- ления (advise loop). Организовав цикл уведомления, клиент может не заботиться об актуальности своей информации, так как при измене-
Гпава 10. Обмен данными между приложениями 359 нии данных сервер сам доловит ему об этом. Уведомление приходит клиенту через callback-функцию как транзакция xtyp_AdvData (см. ниже). Различают две связи с элементом данных: “горячая” (hot) и “теплая” (warm). Горячая связь (hot advise loop) устанавливается в результате тран- закции xtyp_AdvStart автоматически, если при вызове функции DdeClientTransaction не указан флаг xtypf_NoData. При такой связи сервер посылает клиенту обновленные данные вместе с уведомлением об их изменении. Теплая связь (warm advise loop) устанавливается, если при вызове DdeClientTransaction указан флаг xtypf_NoData. При такой связи сервер посылает клиенту только уведомление об изменении данных, и клиент имеет право выбрать: проигнорировать уведомление или за- просить у сервера обновленные данные с помощью транзакции xtyp_Request. Табл.Л0.5. Транзакции, посылаемые с помощью DdeClientTransaction Тип транзакции Описание xtyp_AdvStart Клиент запрашивает уведомление о любом изменении данных еервера. Вместе со значением xtyp_AdvStart мо- гут передаваться флаги xtypf_NoData и xtypf_AckReq. Флаг xtypf_NoData указывает, что сервер должен изве- щать клиента только о факте изменения данных, но сами данные не посылать (по умолчанию^ни посылаются). Флаг xtypf_AckReq сдерживает посылку данных серве- ром, если клиент еще не обработал предыдущий блок данных xtyp_AdvStop Клиент прекращает цикл уведомлений, начатый с помо- щью транзакции xtyp_AdvStart xtyp_Execute Клиент передает серверу команду, которую тот должен выполнить. Команда передается как блок данных в па- раметре Data. Для этой транзакции значение параметра Item (имя элемента данных) должно быть равно нулю xtyp_Poke Клиент передает серверу элемент данных xtyp_Request Клиент запрашивает у сервера элемент данных. Если транзакция синхронная, то дескриптор блока данных возвращается как значение функции. Если транзакция асинхронная, блок данных передается в callback- функцию клиента с уведомлением xtyp_XACT_Complete (см. ниже) Со стороны сервера связь с элементом данных обеспечивается вы- зовом функции DdePostAdvise(Inst: Longint; Topic, Item: HSz): Bool;
360 Программирование в среде Borland Pascal для Windows где Inst - идентификатор программы в библиотеке DDEML; Topic - имя темы; Item - имя элемента данных, с которым установлена связь. Значение функции равно True при отсутствии ошибок. Сервер.должен вызывать DdePostAdvise всякий раз при изменении любого элемента данных, независимо от того, происходит это по за- просам клиентов или по его собственной инициативе. Результатом этого вызова является посылка серверу транзакции xtyp.AdvReq, в ответ на которую он должен вернуть библиотеке DDEML дескриптор глобального блока данных, созданного с помощью функции DdeCreateDataHandle (см. выше). Библиотека DDEML сама рассыла- ет клиентам уведомления и передает при необходимости изменившие- ся данные. Для обработки транзакций клиента сервер определяет callback- функцию и при подключении к DDEML сообщает Windows ее адрес. Клиент тоже определяет callback-функцию - для обработки результа- тов своих асинхронных транзакций и других уведомлений от сервера. Формат callback-функции следующий: DdeCallbackProc(CallType, Fmt: Word; Conv: HConv; HSzl, HSz2: HSz; Data: HDDEData; Datal, Data2: Longint): HDDEData; export; Вы должны сами объявить подобную функцию (название может быть любым) и передать в Ddelnitialize. Важно, чтобы при определе- нии этой функции была включена директива компилятора Smart callbacks - {$К+} (прил. Г). Параметры функции: CallType - тип транзакции; Fmt - формат данных (см. табл. 10.1); Conv - дескриптор соединения; HSzl, HSz2 - имена, зарегистрированные функцией DdeCreateStringHandle. Смысл этих имен зависит от типа транзакции. Например, при установке со- единения HSzl содержит имя темы, a HSz2 - имя службы, при переда- че данных HSzl - имя темы, a HSz2 - имя элемента данных; Data - дескриптор блока данных. Если в транзакции не передаются данные, значение дескриптора Data равно нулю; Datal, Data2 - дополнитель- ная информация, которая зависит от типа транзакции. Значение функции определяется типом транзакции. Если в транзакции запра- шивается блок данных, то он возвращается в качестве значения callback-функции. Типы обрабатываемых callback-функцией транзакций регулируют- ся флагами из табл. 10.3, указанными при подключении программы к DDEML. Если клиент посылает неразрешенную транзакцию, библио- тека DDEML фиксирует ошибку и не передает транзакцию серверу. В зависимости от того, является программа сервером или клиентом, ей могут посылаться различные типы транзакций. Очевидно, что про- граммы, выступающие одновременно в роли сервера и клиента, могут обрабатывать все типы транзакций.
Гпава 10. Обмен данными между приложениями 361 Перечислим транзакции, обрабатываемые сервером: xtyp.Error - критическая ошибка, в результате которой может произойти потеря DDE-данных. Word(Datal) - код ошибки. В на- стоящее время транзакция xtyp_Error посылается только при нехватке памяти (dmlErr_Low_Memory); xtyp_Register - посылается каждому DDE-приложению при регист- рации некоторым сервером новой службы (DdeNameService с пара- метром dns_Register). HSzl - базовое имя службы, HSz2 - имя службы, связанное с конкретным экземпляром программы, вызвавшим DdeNameService. Значение, возвращаемое callback-функцией сервера, игнорируется; xtyp_Unregister - посылается каждому DDE-приложению при уда- лении службы (DdeNameService с параметром dns_Unregister). HSzl - базовое имя службы, HSz2 - имя службы, связанное с конкретным экземпляром программы, вызвавшим DdeNameService. Значение, воз- вращаемое callback-функцией сервера, игнорируется; xtyp_Connect - клиент устанавливает соединение с сервером (с по- мощью функции DdeConnect). HSzl - имя темы, HSz2 - имя службы. PConvContext(Datal) - указатель на запись типа TConvContext, опи- сывающую контекст соединения (код страны, номер кодовой табли- цы, код языка и др.). Bool(Data2) - если True, то клиентом, устанавли- вающим соединение, является это же самое приложение. Чтобы раз- решить соединение, callback-функция сервера должна вернуть ненуле- вое значение; нуль означает отказ; xtyp_WildConnect - клиент устанавливает соединение с сервером (с помощью функции DdeConnect или DdeConnectList), указывая вместо имени службы или имени темы (или вместо того и другого) нуль. HSzl - имя темы, HSz2 - имя службы. PConvContext(Datal) - указа- тель на запись типа TConvContext, описывающую контекст соедине- ния (код страны, номер кодовой таблицы, код языка и др.). Bool(Data2) - если True, то клиентом, устанавливающим соединение, выступает это же самое приложение. Callback-функция сервера долж- на вернуть глобальный блок с массивом записей типа THSZPair. Каж- дая запись содержит два поля: hszSvc - имя поддерживаемой сервером службы, и hszTopic - имя поддерживаемой в рамках службы темы. Концом массива считается запись с нулевыми значениями полей. Если сервер отвергает транзакцию xtyp.WildConnect, cajlback-функция должна вернуть нулевое значение; xtyp_Connect_Confirm - посылается серверу как подтверждение ус- пешной установки соединения с клиентом. В параметре Conv переда- ется дескриптор соединения. HSzl - имя темы, HSz2 - имя службы. Bool(Data2) - если True, то клиентом, установившим соединение, яв- ляется это же самое приложение. Значение, возвращаемое callback- функцией сервера, игнорируется;
362 Программирование в среде Borland Pascal для Windows xtypJDisconnect - посылается партнеру в соединении (серверу или клиенту) при вызове DdeDisconnect. Bool(Data2) - если True, то парт- нером в соединении является это же самое приложение. Значение, воз- вращаемое callback-функцией сервера, игнорируется; xtyp_AdvStart - клиент запрашивает уведомление о любом измене- нии данных сервера. HSzl - имя темы, HSz2 - имя элемента данных, с которым устанавливается связь. Транзакция xtyp_AdvStart посылает- ся клиентом через DdeClientTransaction и передается в callback- функцию сервера. Чтобы разрешить цикл уведомления, callback- функция сервера должна вернуть ненулевое значение; нуль означает отказ; xtyp_AdvStop - клиент прекращает цикл уведомлений, начатый с помощью транзакции xtyp_AdvStart. HSzl - имя темы, HSz2 - имя элемента данных. Значение, возвращаемое callback-функцией сервера, игнорируется; xtyp_AdvReq - посылается серверу, вызвавшему DdePostAdvise. Транзакция xtyp_AdvReq посылается серверу столько раз, сколько “горячих” связей установлено клиентами с его элементом данных. HSzl - имя темы, HSz2 - имя элемента данных. Word(Datal) - число оставшихся посылок xtyp_AdvReq; для последней посылки оно равно нулю. В ответ на транзакцию сервер должен вернуть библиотеке DDEML дескриптор глобального блока данных, созданного с помо- щью DdeCreateDataHandle; xtypJExecute - клиент запрашивает выполнение команды. HSzl - имя темы, Data - дескриптор глобального блока данных, содержаще- го команду. Чтобы получить доступ к команде, необходимо вызвать функцию DdeAccessData или DdeGetData. Сервер должен вернуть одно из следующих значений: dde_FAck - если транзакция успешно обработана; dde_FBusy - если сервер занят и не может обработать транзакцию; dde_FNotProcessed - если сервер отвергает транзакцию; xtyp_Poke - клиент передает серверу элемент данных. HSzl - имя темы, HSz2 - имя элемента данных. Data - дескриптор глобального блока данных. Транзакция xtyp_Poke посылается клиентом с помо- щью DdeClientTransaction и приходит в callback-функцию сервера. Сервер должен вернуть одно из следующих значений: dde_FAck - если транзакция успешно обработана; dde_FBusy - если сервер занят и не может обработать транзакцию; dde_FN о t Pro cessed - если сервер от- вергает транзакцию; xtyp_Request - клиент запрашивает у сервера элемент данных. HSzl - имя темы, HSz2 - имя элемента данных. В общем случае сервер должен создать глобальный блок, заполнить его данными и вернуть библиотеке DDEML. Если сервер хранит элемент данных в виде гло- бального блока, созданного с флагом hdata_AppOwned (см. DdeCreateDataHandle), то он может просто вернуть его дескриптор в
Гпава 10. Обмен данными между приложениями 363 качестве значения callback-функции. Если сервер возвращает нуль, то транзакция отвергается, и клиент не получает данных. Перечислим транзакции, обрабатываемые клиентом: xtyp_Error, xtyp_Register, xtyp_Unregister, xtyp_Disconnect - описа- ны выше; xtyp_AdvData - посылается клиенту при изменении у сервера дан- ных. HSzl - имя темы, HSz2 - имя элемента данных. Data - дескрип- тор глобального блока данных. Получение этой транзакции стано- вится возможным только после установки с элементом данных серве- ра цикла уведомления (см. описание транзакции xtyp_AdvStart); xtyp_XACT_Complete - сервер завершил выполнение асинхронной транзакции. HSzl - имя темы, HSz2 - имя элемента данных. Data - дескриптор глобального блока данных, если в результате транзакции сервер возвращает клиенту данные. Datal - идентификатор асинхрон- ной транзакции. Word(Data2) - флаги состояния (в следующих верси- ях DDEML они могут не поддерживаться). Кроме рассмотренных транзакций, в Windows предусмотрена еще одна - xtyp_Moiiitor. Она передается приложению только в том слу- чае, если при подключении к DDEML (Ddelnitialize) оно было зареги- стрировано как монитор (AppClass_Monitor). Приложение-монитор получает все события, связанные с работой DDEML, и иногда созда- ется для отладки. Подробное описание транзакции xtyp_Monitor вы найдете в руководстве по Borland Pascal. 10.3.7. ПРИМЕР ДИНАМИЧЕСКОГО ОБМЕНА ДАННЫМИ Для демонстрации возможностей DDE разработаем программу- сервер (назовем ее DDESRVR) и программу-клиент (DDECLNT). Сделаем так, чтобы процесс рисования с помощью мыши в окне про- граммы-сервера одновременно отображался в окнах запущенных про- грамм-клиентов (рис. 10.1). DDE-протокол идеально подходит для решения данной задачи. В качестве элемента данных принимается пара чисел - координаты кур- сора мыши относительно окна. Сервер хранит последнюю позицию курсора и извещает клиентов о любом ее изменении. Изменение пози- ции курсора происходит динамически в результате рисования в окне с помощью мыши. Сразу при запуске клиент устанавливает соединение с сервером, а затем инициирует “горячий” цикл уведомления. В этом режиме сервер уведомляет клиентов о каждом изменении элемента данных и передает им изменившиеся данные. Получая очередную по- зицию указателя мыши, программа-клиент рисует в своем окне про- должение линии от последней точки до вновь полученной, тем самым обеспечивая динамическое отображение процесса рисования. Для фиксации разрывов линий сервер посылает клиентам точку с коорди-
364 Программирование в среде Borland Pascal для Windows Рис. 10.1. Окна программ DDESRVR и DDECLNT в процессе работы натами (-Maxint, -Maxint), где Maxint - предопределенная константа языка Borland Pascal, равная 32767. { Рассмотрим сначала реализацию DDE-сервера. Мы сочли целесо- образным сразу привести текст готовой программы, а затем его про- комментировать. program DdeSrvr; uses WinTypes, WinProcs {$ifdefVer80}, Messages {$endif}, Strings, OWindows, DDEML; const em_Dde I nit Failed = -6; { ошибка инициализации DDE-сервера } type PDdeServerWindow = ATDdeServerWindow; TDdeServerWindow = object(TWindow) Service: HSz; { имя службы} Topic: HSz; { имя темы } Item: HSz; { имя элемента данных } ItemData: HDDEData; { элемент данных} constructor lnit(AParent: PWindowsObject; ATitle: PChar); procedure SetupWindow; virtual; procedure WMDestroy(var Msg: TMessage); virtual wm_First + wm_Destroy; function DdeCallback(CallType, Fmt: Word; Conv: HConv; HSzl, HSz2: HSz; Data: HDDEData; Datal, Data2: Longint): HDDEData; function Connect(AService, ATopic: HSz): HDDEData; function WildConnect(AService, ATopic: HSz; Fmt: Word): HDDEData; function StartAdvise(Altem: HSz): HDDEData; function StopAdvise(Altem: HSz): HDDEData; function RequestData(Altem: HSz; Fmt: Word): HDDEData; function PokeData(Altem: HSz; Fmt: Word; Data: HDDEData): HDDEData; procedure SetData(Ptr: Pointer; Size: Longint); end; PMainWindow = ATMainWindow;
Гпава 10. Обмен данными между приложениями 365 TMainWindow = object(TDdeServerWindow) ButtonDown: Boolean; { нажата ли кнопка мыши } HoldDC: HDC; { вспомогательный контекст дисплея} constructor lnit(ATitle: PChar); function GetClassName: PChar; virtual; procedure GetWindowClass(var AWndClass: TWndClass); virtual; procedure SetPoint(X, Y: Integer); 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; PDdeServerApp = ATDdeServerApp; TDdeServerApp = object(TApplication) procedure Initlnstance; virtual; destructor Done; virtual; procedure Error(ErrorCode: Integer); virtual; procedure InitMainWindow; virtual; end; const Ddelnst: Longint = 0; {дескриптор приложения в DDEML } DdeWindow: PDdeServerWindow = nil; { окно DDE-сервера } { Callback-функция обработки DDE-транзакций } function DdeCallbackProc(CallType, Fmt: Word; Conv: HConv; HSzl, HSz2: HSz; Data: HDDEData; Datal, Data2: Longint): HDDEData; export; begin if DdeWindow <> nil then DdeCallbackProc := DdeWindowA.DdeCallback( CallType, Fmt, Conv, HSzl, HSz2, Data, Datal, Data2); end; { Конструирует оконный объект и инициализирует его поля } constructor TDdeServerWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); Service := 0; Topic := 0; Item := 0; ItemData := 0; end; f { После создания окна регистрирует службу с именем 'DdeSrvr'} procedure TDdeServerWindow.SetupWindow; begin inherited SetupWindow; Service := DdeCreateStringHandle(Ddelnst, ’DdeSrvr’, cp_WinAnsi); Topic := DdeCreateStringHandle(Ddelnst, ’Draw’, cp_WinAnsi); Item := DdeCreateStringHandle(Ddelnst, ’Point’, cp_WinAnsi); DdeWindow := @Self;
366 Программирование в среде Borland Pascal для Windows DdeNameService(Ddelnst, Service, 0, dnsRegister); end; { Освобождает память при уничтожении окна } procedure TDdeServerWindow.WMDestroy(var Msg: TMessage); begin DdeWindow := nil; if ItemData <> 0 then DdeFreeDataHandle(ltemData); if Service <> 0 then DdeFreeStringHandle(Ddelnst, Service); if Topic <> 0 then DdeFreeStringHandle(Ddelnst, Topic); if Item <> 0 then DdeFreeStringHandle(Ddelnst, Item); inherited WMDestroy(Msg); end; { Передает принятую транзакцию нужному методу оконного объекта } function TDdeServerWindow.DdeCailback(CallType, Fmt: Word; Conv: HConv; HSzl, HSz2: HSz; Data: HDDEData; Datal, Data2: Longint): HDDEData; begin DdeCallback := 0; case CallType of xtyp_Connect: DdeCallback := Connect(HSz2, HSzl); xtypJA/ildConnect: DdeCallback := WildConnect(HSz2, HSzl, Fmt); xtyp_AdvStart: DdeCallback := StartAdvise(HSz2); xtyp_AdvStop: DdeCallback := StopAdvise(HSz2); xtyp_AdvReq, xtyp Request: DdeCallback := RequestData(HSz2, Fmt); xtyp_Poke: DdeCallback := PokeData(HSz2, Fmt, Data); end; end; { Разрешает или запрещает соединение в ответ на xtypjConnect} function TDdeServerWindow.Connect(AService, ATopic: HSz): HDDEData; begin Connect := 0; if (DdeCmpStringHandles(AService, Service) = 0) and (DdeCmpStringHandles(ATopic, Topic) = 0) then Connect := 1; end; { Возвращает имя службы и темы в ответ на транзакцию xtyp_WildConnect} function TDdeServerWindow.WildConnect(AService, ATopic: HSz; Fmt: Word): HDDEData; var Pairs: array [0..1] of THSZPair; begin WildConnect := 0; Pairs[0].hszSvc := Service; Pairs[0].hszTopic := Topic; Pairs[1].hszSvc := 0; Pairs[1].hszTopic := 0; if ((Service = 0) and (Topic = 0)) or ((Service = 0) and (DdeCmpStringHandles(ATopic, Topic) = 0)) or ((DdeCmpStringHandles(AService, Service) = 0) and (Topic = 0)) then WildConnect := DdeCreateDataHandle(Ddeinst, ©Pairs, SizeOf(Pairs), 0, 0, Fmt, 0);
Гпава 10. Обмен данными между приложениями 367 end; { Разрешает связь с элементом данных в ответ на xtyp_AdvStart} function TDdeServerWindow.StartAdvise(Altem: HSz): HDDEData; begin StartAdvise := 0; if DdeCmpStringHandles(Altem, Item) = 0 then StartAdvise := 1; end; { Завершает связь с элементом данных в ответ на xtyp_AdvStop } function TDdeServerWindow.StopAdvise(Aitem: HSz): HDDEData; begin StopAdvise := 0; if DdeCmpStringHandles(Altem, Item) = 0 then StopAdvise := 1; end; {Возвращает элемент данных в ответ на xtyp_AdvReq и xtyp_Request} function TDdeServerWindow.RequestData(Altem: HSz; Fmt: Word): HDDEData; begin RequestData := 0; if (DdeCmpStringHandles(Altem, Item) = 0) and (Fmt = cf_Text) then RequestData := ItemData; end; { Принимает от клиента данные в транзакции xtyp__Poke } function TDdeServerWindow.PokeData(Altem: HSz; Fmt: Word; Data: HDDEData): HDDEData; var Ptr: Pointer; Size: Longint; E: Integer; begin PokeData := dde_FNotProcessed; if (DdeCmpStringHandles(Altem, Item) = 0) and (Fmt = cf_Text) then begin if ItemData <> 0 then DdeFreeDataHandle(ltemData); Ptr := DdeAccessData(Data, @Size); ItemData := DdeCreateDataHandle(Ddelnst, Ptr, Size, 0, Item, cf_Text, hdata_AppOwned); DdeUnaccessData(Data); DdePostAdvise(Ddelnst, Topic, Item); PokeData := dde_FAck; end; end; { Устанавливает новое значение для элемента данных ItemData } procedure TDdeServerWindow.SetData(Ptr: Pointer; Size: Longint); begin * if ItemData <> 0 then DdeFreeDataHandle(ltemData); ItemData := DdeCreateDataHandle( Ddelnst, Ptr, Size, 0, Item, cf_Text, hdata_AppOwned); DdePostAdvise(Ddelnst, Topic, Item); end; { Конструирует оконный объект и инициализирует его поля } constructor TMainWindow.lnit(ATitle: PChar);
368 Программирование в среде Borland Pascal для Windows begin inherited lnit(nil, ATitle); ButtonDown := False; HoldDC := 0; end; { Возвращает имя нового оконного класса } function TMainWindow.GetClassName: PChar; begin GetClassName := 'DDEServerWindow'; 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.SetPoint(X, Y: Integer); var Str: array [0..63] of Char; Point: TPoint; begin Point.X := X; ' Point. Y:= Y; WVSPrintF(Str, '%i,%i', Point); SetData(@Str, StrLen(Str) + 1); 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); SetPoint(Msg.LParamLo, Msg.LParamHi); end; end; { При отпускании левой кнопки мыши завершает рисование } procedure TMainWindow.WMLButtonUp(var Msg: TMessage); begin if ButtonDown then begin SetPoint(-Maxlnt, -Maxlnt); ReleaseDC(HWindow, HoldDC); ReleaseCapture; ButtonDown := False; end;
Гпава 10. Обмен данными между приложениями 369 end; { При передвижении мыши рисует приращение линии } procedure TMainWindow.WMMouseMove(var Msg: TMessage); begin if ButtonDown then begin LineTo(HoldDC, Msg.LParamLo, Msg.LParamHi); SetPoint(Msg.LParamLo, Msg.LParamHi); end; end; { Очищает рабочую область окна, удаляя нарисованное изображение } procedure TMainWindow.WMLButtonDblClk(var Msg: TMessage); begin lnvalidateRect(HWindow, nil, True); end; { Регистрирует приложение в DDEML и инициализирует его } procedure TDdeServerApp. Initlnstance; begin Ddelnst := 0; if Ddelnitialize(Ddelnst, DdeCallbackProc, 0, 0) = dmlErr_No_Error then inherited Initlnstance; if DdeGetLastError(Ddelnst) <> dmlErr_No_Error then Status := em_DdelnitFailed; end; { Отключает приложение от DDEML и разрушает прикладной объект } destructor TDdeServerApp. Done; begin if Ddelnst <> 0 then DdeUninitialize(Ddelnst); inherited Done; end; { Обрабатывает ошибки прикладного объекта } procedure TDdeServerApp.Error(ErrorCode: Integer); begin if ErrorCode = em_DdelnitFailed then MessageBox(0, 'Cannot initialize DDE server', Name, mbJconExclamation + mb_Ok); inherited Error(ErrorCode); end; { Создает объект главного окна программы } procedure TDdeServerApp.lnitMainWindow; begin MainWindow := New(PMainWindow, InitfServer Demo')); end; var 0 DdeServerApp: TDdeServerApp; begin DdeServerApp.lnitCDdeSrvr*); DdeServerApp.Run; DdeServerApp. Done; end.
370 Программирование в среде Borland Pascal для Windows Анализ этой достаточно сложной программы удобнее всего начать с прикладного объекта TDdeServerApp, который обеспечивает ини- циализацию и деинициализацию программного экземпляра в библио- теке DDEML, обработку связанных с этими действиями ошибок и создание окна сервера. Подключение программного экземпляра в DDEML выполняет ме- тод TDdeServerApp.InitInstance. Для этого он вызывает функцию Ddelnitialize, в которую вместе с другими параметрами передает адрес callback-функции DdeCallbackProc. Обратите внимание на то, что в Ddelnitialize передается непосредственный адрес функции DdeCallbackProc, что становится возможным благодаря включению директивы компилятора $К в самом начале программы. Ddelnitialize возвращает дескриптор программного экземпляра в DDEML, сохра- няемый в глобальной переменной Ddelnst. При успешном подключе- нии к DDEML вызывается метод Initlnstance предка, который выпол- няет стандартную инициализацию и создает окно программы. Если в результате всех этих действий обнаруживается ошибка DDEML, о чем свидетельствует результат функции DdeGetLastError, то в поле Status объекта TDdeServerApp заносится значение em_DdeInitFailed. Отключение программы от DDEML выполняет деструктор Done. Он вызывает функцию DdeUninitialize, передавая в нее дескриптор Ddelnst. С целью обработки ошибки em_DdeInitFailed, которая может воз- никнуть при инициализации сервера, в объекте TDdeServerApp пере- крыт виртуальный метод Error. Если код ошибки совпадает с em_DdeInitFailed, то Error отображает на экране окно с сообщением 'Cannot initialize DDE server'. Работа метода TDdeServerApp.InitMainWindow очевидна - это создание оконного объекта. Так как поведение сервера определяется действиями пользователя в окне, вся функциональность, связанная с взаимодействием через DDE помещена в оконный объект TDdeServerWindow. Этот объект определяет общие свойства ассоциированного с окном сервера, кото- рый поддерживает один элемент данных 'Point' в рамках единственной темы 'Draw'. Данные представляются в стандартном формате буфера обмена с кодом cf_Text (текстовая строка, заканчивающаяся нуль- терминатором). Рассмотрим поля и методы объекта TDdeServerWindow. Поля объекта имеют следующее назначение: Service, Topic, Item - соответственно имена службы, темы и элемента данных; ItemData - дескриптор DDE-данных. В конструкторе Init перечисленные поля инициализируются нулем. В методе SetupWindow оконного объекта с помощью функции DdeCreateStringHandle создаются имена службы, темы и элемента
Гпава 10. Обмен данными между приложениями 371 данных, после чего вызовом DdeNameService регистрируется служба. Отметим, что перед регистрацией службы выполняется одно важное действие: глобальному указателю DdeWindow присваивается значение @Self. Указатель DdeWindow объявлен с типом PDdeServerWindow и ссылается на оконный объект, которому переадресуются транзакции, посылаемые библиотекой DDEML (см. функцию DdeCallbackProc). В методе WMDestroy, вызываемом при закрытии окна, осуществ- ляются обратные действия: указателю DdeWindow присваивается значение nil, освобождаются оставшиеся после работы DDE-данные, а также удаляются имена службы, темы и элемента данных. Посылаемые серверу транзакции принимаются callback-функцией DdeCallbackProc, которая регистрируется в DDEML при вызове Ddelnitialize. Транзакции удобнее всего обрабатывать в оконном объекте, поэтому DdeCallbackProc просто перенаправляет вызов к методу DdeCallback объекта, адресуемого указателем DdeWindow. Метод TDdeServerWindow.DdeCallback обеспечивает первичную обработку транзакций. Он выбирает в соответствии с типом транзак- ции нужный виртуальный метод объекта и передает ему управление. Обрабатываются следующие типы транзакций: xtyp_Connect, xtypJWildConnect, xtyp.AdvStart, xtyp.AdvStop, xtyp_AdvReq, xtyp_Request, xtypJPoke. Рассмотрим их по порядку. Транзакция xtyp_Connect посылается серверу, когда клиент пыта- ется установить с ним соединение с помощью функции DdeConnect. Отметим, что сервер получает эту транзакцию только в том случае, если клиент указал действительное имя службы и отличное от нуля имя темы. Для того чтобы разрешить установку соединения, сервер должен вернуть из callback-функции ненулевое значение. Транзакция xtyp_Connect обрабатывается объектом TDdeServerWindow в вирту- альном методе Connect. Метод Connect проверяет, указал ли клиент правильные имена службы и темы, и разрешает соединение, возвра- щая значение 1. В противном случае возвращается 0 как отказ от ус- тановки соединения. Транзакция xtypJWildConnect передается callback-функции сервера в том случае, если при установке соединения клиент указывает в каче- стве имени службы или имени темы (или вместо имен службы и темы) нуль. В последнем случае соединение устанавливается с первым по- павшимся сервером. Обрабатывая эту транзакцию, сервер возвращает дескриптор блока данных, содержащего масси! записей типа THSZPair. Каждая запись кодирует пару служба-тема. Весь массив завершается записью с нулевыми значениями полей. Транзакция xtypJWildConnect обрабатывается объектом TDdeServerWindow в виртуальном методе WildConnect. Этот метод формирует блок дан- ных, содержащий единственную пару служба-тема, и возвращает его как результат обработки транзакции в одном из трех случаев:
372 Программирование в среде Borland Pascal для Windows • клиент указал нулевое имя и для службы и для темы; • клиент указал нулевое имя службы и существующее имя темы; • клиент указал существующее имя службы и нулевое имя темы. Транзакция xtyp_AdvStart предназначена для установки цикла уведомления и посылается серверу клиентом с помощью функции DdeClientTransaction. Цикл уведомления всегда устанавливается для конкретного элемента данных. Данная транзакция обрабатывается объектом TDdeServerWindow в виртуальном методе StartAdvise. Ме- тод StartAdvise проверяет, указал ли клиент имя существующего эле- мента данных, и разрешает установить с ним связь, возвращая значе- ние 1. В противном случае возвращается нулевое значение, и связь не устанавливается. Транзакция xtyp_AdvStop извещает сервер о прекращении цикла уведомлений, начатого с помощью транзакции xtyp_AdvStart. Данная транзакция обрабатывается в виртуальном методе StopAdvise, кото- рый по умолчанию не выполняет никаких действий. Транзакция xtyp_AdvReq используется библиотекой DDEML для запроса данных у сервера в “горячем” цикле уведомления. Она авто- матически посылается серверу при вызове им функции DdePostAdvise. Транзакция xtyp_Request тоже предназначена для получения дан- ных от сервера, но посылается клиентом с помощью функции DdeClientTransaction. Транзакции xtyp.AdvReq и xtyp_Request обрабатываются в одном и том же виртуальном методе RequestData. Метод проверяет, сущест- вует ли элемент данных с указанным именем и имеет ли он формат cf_Text, и возвращает дескриптор данных ItemData. Транзакция xtyp_Poke позволяет клиентам передавать данные сер- веру и посылается с помощью функции DdeClientTransaction. Хотя при взаимодействии программ DDESRVR и DDECLNT эта тран- закция не используется, реакция на нее определена. Это сделано наме- ренно, чтобы придать серверу целостность. Транзакция xtyp_Poke обрабатывается в виртуальном методе PokeData, которому пере- даются имя элемента данных, код формата и дескриптор блока данных. Если элемент данных с указанным именем существует и имеет формат cf_Text, то PokeData освобождает существующий элемент данных ItemData и создает новый, копируя в него содержимое переданного блока. После этого PokeData рассылает клиентам уведомления об изменении данных, вызывая DdePostAdvise, и воз- вращает значение ddeJFAck в качестве результата обработки тран- закции. Для установки новых данных самим сервером в объекте TDdeServerWindow определен вспомогательный метод SetData, в ко- торый передается указатель на данные и их размер. SetData освобож- дает существующий блок данных и создает новый, присваивая его
Гпава 10. Обмен данными между приложениями 373 дескриптор полю ItemData. После этого он вызывает функцию DdePostAdvise для рассылки уведомлений клиентам. Рассылку осуще- ствляет библиотека DDEML, которая сначала обращается к серверу с транзакцией xtyp_AdvReq для получения глобального блока данных, а затем передает его клиенту с транзакцией xtyp_AdvData. Транзак- ция xtyp_AdvReq посылается серверу столько раз, сколько клиентов установило “горячую” связь с элементом данных. Объект TDdeServerWindow реализует базовые механизмы работы простейшего сервера; его легко приспособить для конкретных нужд, в частности, для динамического отображения процесса рисования. С этой целью в программе DDESRVR определен производный от TDdeServerWindow объект TMainWindow. TMainWindow представляет главное окно программы и обеспечи- вает рисование в нем с помощью мыши. Этот вопрос подробно рас- смотрен в гл. 4, поэтому мы полагаем, что вы без труда разберетесь с работой объекта. Поясним только работу метода SetPoint, вызывае- мого из методов ответа на сообщения мыши. SetPoint обновляет дан- ные сервера, вызывая метод SetData. Так как сервер хранит данные в стандартном формате буфера обмена cf_Text, т.е. в текстовом виде, то перед вызовом SetData координаты мыши преобразуются в ASCIIZ- строку, которая содержит два числа, разделенных запятой. Итак, механизм работы сервера DDESRVR нам понятен. Рассмот- рим теперь программу DDECLNT, реализующую работу клиента. program DdeCInt; {$К+} uses WinTypes, WinProcs {$ifdefVer80}, Messages {$endif}, Strings, OWindows, DDEML; const em_DdelnitFailed = -6; { ошибка инициализации DDE-клиента } type PDdeClientWindow = ATDdeClientWindow; TDdeClientWindow = object(TWindow) { имя службы } { имя темы } { имя элемента данных } { дескриптор соединения } Service: HSz; Topic: HSz; Item: HSz; Conv: HConv; constructor lnit(AParent: PWindowsObject; ATitle: PChar); procedure SetupWindow; virtual; procedure WMDestroy(var Msg: TMessage); " virtual wm_First + wm_Destroy; function DdeCallback(CallType, Fmt: Word; AConv: HConv; HSzl, HSz2: HSz; Data: HDDEData; Datal, Data2: Longint): HDDEData; virtual; function AdviseData(Altem: HSz; Fmt: Word; Data: HDDEData): HDDEData; virtual; procedure Disconnect; virtual; procedure ReceiveData(Ptr: Pointer; Size: Longint); virtual;
374 Программирование в среде Borland Pascal для Windows end; PMainWindow = ATMainWindow; TMainWindow = object(TDdeClientWindow) LastPoint: TPoint; { последняя позиция мыши } constructor lnit(ATitle: PChar); function GetClassName: PChar; virtual; procedure GetWindowClass(var AWndClass: TWndClass); virtual; procedure ReceiveData(Ptr: Pointer; Size: Longint); virtual; procedure WMLButtonDblClk(var Msg: TMessage); virtual wm_First + wm_LButtonDblClk; end; i PDdeClientApp = ATDdeClientApp; TDdeClientApp = object(TApplication) procedure Initlnstance; virtual; destructor Done; virtual; procedure Error(ErrorCode: Integer); virtual; procedure InitMainWindow; virtual; end; const Ddelnst: Longint = 0; { дескриптор приложения в DDEML } DdeWindow: PDdeClientWindow = nil; { окно DDE-клиента } { Callback-функция обработки DDE-транзакций } function DdeCallbackProc(CallType, Fmt: Word; Conv: HConv; HSzl, HSz2: HSz; Data: HDDEData; Datal, Data2: Longint): HDDEData; export; begin if DdeWindow <> nil then DdeCallbackProc := DdeWindowA.DdeCallback( CallType, Fmt, Conv, HSzl, HSz2, Data, Datal, Data2); end; { Конструирует оконный объект и инициализирует его поля } constructor TDdeClientWindow.lnit(AParent: PWindowsObject; ATitle: PChar); begin inherited lnit(AParent, ATitle); Service := 0; Topic := 0; Item := 0; Conv := 0; end; { После создания окна устанавливает соединение с сервером и налаживает "горячую” связь с элементом данных 'Point’} procedure TDdeClientWindow.SetupWindow; begin inherited SetupWindow; Service := DdeCreateStringHandle(Ddelnst, 'DdeSrvr', cp_WinAnsi); Topic := DdeCreateStringHandle(Ddelnst, 'Draw', cp_WinAnsi); Item := DdeCreateStringHandle(Ddelnst, 'Point', cp_WinAnsi); DdeWindow := @Self; Conv := DdeConnect(Ddelnst, Service, Topic, nil); if Conv <> 0 then DdeClientTransaction(nil, 0, Conv, Item, cf_Text, xtyp_AdvStart, 10000, nil); end;
Гпава 10. Обмен данными между приложениями 375 {Разрывает соединение и освобождает память при уничтожении окна} procedure TDdeClientWindow.WMDestroy(var Msg: TMessage); begin if Conv <> 0 then DdeDisconnect(Conv); DdeWindow := nil; if Service <> 0 then DdeFreeStringHandle(Ddelnst, Service); if Topic <> 0 then DdeFreeStringHandle(Ddelnst, Topic); if Item <> 0 then DdeFreeStringHandle(Ddelnst, Item); inherited WMDestroy(Msg); end; { Передает принятую транзакцию нужному методу оконного объекта } function TDdeClientWindow.DdeCallback(CallType, Fmt: Word; AConv: HConv; HSzl, HSz2: HSz; Data: HDDEData; Datal, Data2: Longint): HDDEData; begin DdeCallback := 0; case CallType of xtyp_AdvData: DdeCallback := AdviseData(HSz2, Fmt, Data); xtyp_Disconnect: Disconnect; end; end; { Принимает данные в транзакции xtyp_AdvData } function TDdeClientWindow.AdviseData(Altem: HSz; Fmt: Word; Data: HDDEData): HDDEData; var Ptr: Pointer; Size: Longint; begin AdviseData := dde_FNotProcessed; if (DdeCmpStringHandles(Altem, Item) = 0) and (Fmt = cf_Text) and (Data <> 0) then begin Ptr := DdeAccessData(Data, @Size); ReceiveData(Ptr, Size); DdeUnaccessData(Data); AdviseData := dde_FAck; end; end; { Обнуляет дескриптор соединения в ответ на транзакцию xtyp_Disconnect} procedure TDdeClientWindow. Disconnect; begin Conv := 0; end; 0 { Выполняет непосредственный прием и отображение данных } procedure TDdeClientWindow.ReceiveData(Ptr: Pointer; Size: Longint); begin end; { Конструирует оконный объект и инициализирует его поля } constructor TMainWindow.lnit(ATitle: PChar); begin inherited lnit(nil, ATitle);
376 Программирование в среде Borland Pascal для Windows LastPoint.X := -Maxlnt; LastPoint.Y := -Maxlnt; end; { Возвращает имя нового оконного класса } function TMainWindow.GetClassName: PChar; begin GetClassName := 'DDECIientWindow'; 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.ReceiveData(Ptr: Pointer; Size: Longint); var Str1, Str2: PChar; Point: TPoint; E: Integer; DC: HDC; begin Str1 != Ptr; Str2 := StrScan(Str1, ’,'); Str2A := #0; lnc(Str2); Val(Str1, Point.X, E); Val(Str2, Point.Y, E); if (LastPoint.X <> -Maxlnt) and (LastPoint.Y <> -Maxlnt) and (Point.X <> -Maxlnt) and (Point.Y <> -Maxlnt) then begin DC := GetDC(HWindow); MoveTo(DC, LastPoint.X, LastPoint.Y); LineTo(DC, Point.X, Point.Y); ReleaseDC(HWindow, DC); end; LastPoint := Point; end; { По двойному щелчку мыши очищает окно } procedure TMainWindow.WMLButtonDblClk(var Msg: TMessage); begin lnvalidateRect(HWindow, nil, True); end; { Регистрирует приложение в DDEML и инициализирует его } procedure TDdeClientApp. Initlnstance; begin Ddelnst := 0; if Ddelnitialize(Ddelnst, DdeCallbackProc, AppCmd_ClientOnly, 0) = dmlErr_No_Error then inherited Initlnstance;
Гпава 10. Обмен данными между приложениями 377 if DdeGetLastError(Ddelnst) <> dmlErr_No_Error then Status := em_DdelnitFailed; end; { Отключает приложение от DDEML и разрушает прикладной объект } destructor TDdeClientApp. Done; begin if Ddelnst <> 0 then DdeUninitialize(Ddelnst); inherited Done; end; { Обрабатывает ошибки прикладного объекта } procedure TDdeClientApp.Error(ErrorCode: Integer); begin if ErrorCode = em_DdelnitFailed then MessageBox(0, 'Cannot initialize DDE Client’, Name, mbJconExclamation + mb_Ok); inherited Error(ErrorCode); end; { Создает объект главного окна программы } procedure TDdeClientApp. InitMainWindow; begin MainWindow := New(PMainWindow, InitfClient Demo')); end; var DdeClientApp: TDdeClientApp; begin DdeClientApp. Init('DdeClnt'); DdeClientApp.Run; DdeClientApp. Done; end. Программа DDECLNT устроена аналогично DDESRVR. Инициа- лизация и деинициализация программного экземпляра в библиотеке DDEML, а также обработка связанных с этими действиями ошибок выполняются прикладным объектом (TDdeClientApp). При инициали- зации функция Ddelnitialize вызывается с флагом AppCmd_ClientOnly, который указывает библиотеке DDEML, что программа работает только как DDE-клиент. Связанная с DDE функциональность помещена в оконный объект TDdeClientWindow, который определяет общие свойства клиента, отображающего в окне некоторый динамический процесс. Рассмот- рим кратко поля и методы этого объекта. Поля Service, Topic, Item хранят соответственно имена службы, те- мы и элемента данных; Conv - это дескриптор DDE-соединения. В конструкторе Init перечисленные поля инициализируются нулем. Метод SetupWindow, вызываемый при открытии окна, создает имена службы, темы и элемента данных, присваивает указателю DdeWindow значение @Self (для переадресации DDE-транзакций оконному объекту), а также устанавливает соединение с сервером, вызывая функцию DdeConnect. Дескриптор соединения присваивается
378 Программирование в среде Borland Pascal для Windows полю Conv. После этого клиент устанавливает “горячую” связь с эле- ментом данных Item или, говоря иначе, запускает цикл уведомлений. Для этого с помощью функции DdeClientTransaction серверу посыла- ется транзакция xtyp_AdvStart. В результате сервер начинает уведом- лять клиента о любом изменении данных. Метод WMDestroy, вызываемый при закрытии окна, разрывает соединение, присваивает указателю DdeWindow значение nil, а также удаляет имена службы, темы и элемента данных. Транзакции, посылаемые серверу, принимаются callback-функцией DdeCallbackProc и переадресуются методу DdeCallback оконного объ- екта. Последний выбирает в соответствии с типом транзакции нуж- ный виртуальный метод объекта и передает ему управление. Объект TDdeClientWindow обрабатывает всего два типа транзакций: xtyp_AdvData и xtyp_Disconnect. Транзакция xtyp_AdvData посылается клиенту при изменении дан- ных сервера. С ее помощью клиент получает данные в “горячем” цик- ле уведомления. Данная транзакция обрабатывается в методе AdviseData. Если уведомление поступило для элемента данных с пра- вильным именем и в правильном формате и дескриптор блока данных не равен нулю, то AdviseData выполняет следующие действия: с по- мощью функции DdeAccessData закрепляет блок данных в памяти, вызывает виртуальный метод ReceiveData для приема данных, рас- крепляет данные с помощью DdeUnaccessData и, наконец, возвращает значение ddeJFAck как результат обработки транзакции. В против- ном случае возвращается значение dde_FNotProcessed как свидетель- ство того, что транзакция не была обработана. Виртуальный метод ReceiveData предназначен для выполнения с данными конкретных действий и в объекте TDdeClientWindow является пустым. Транзакция xtyp_Disconnect посылается при разрыве соединения. Программа DDECLNT получает эту транзакцию, если сервер DDESRVR завершается раньше ее. Транзакция xtypJDisconnect обра- батывается в виртуальном методе Disconnect, который просто уста- навливает дескриптор соединения Conv в нуль. Производный от TDdeClientWindow объект TMainWindow пред- ставляет главное окно программы и обеспечивает отображение полу- чаемых от сервера данных. Для этого в нем перекрыт виртуальный метод ReceiveData, который преобразует данные (координаты курсо- ра мыши) из текстового представления в числовое и рисует в окне продолжение линии. Отрезок линии рисуется от предыдущей позиции до вновь полученной. Предыдущая позиция курсора хранится в поле LastPoint объекта TMainWindow и обновляется методом ReceiveData. Если предыдущая или вновь полученная позиция курсора имеет коор- динаты (-Maxint, -Maxint), то это интерпретируется как разрыв линии.
Гпава 10. Обмен данными между приложениями 379 10.3.8. DDE В ЛОКАЛЬНОЙ СЕТИ В Windows for Workgroups 3.1х и Windows 95 существует встроенная поддержка одноранговой локальной сети и возможность взаимодей- ствия через DDE программ, расположенных на разных компьютерах. Сетевой маршрутизатор DDE-транзакций (Network DDE) прозрачен для клиента и сервера и практически не требует их переделки при за- пуске на разных компьютерах. Это позволяет разрабатывать и отла- живать DDE-программы на одном компьютере, а затем переносить на разные. Однако если вы просто запустите клиента и сервер на разных компьютерах, то потерпите неудачу. Дело в том, что Windows защи- щает компьютер, на котором работает, от вторжения через локальную сеть “троянских коней” соседей. Чтобы соседние компьютеры могли разделять ресурсы (диск, принтер, DDE-серверы) вашего компьютера, их следует предоставить в совместное пользование, т.е. сделать разделяемыми (shared). Пользо- ватели других подключенных в сеть компьютеров имеют доступ толь- ко к предоставленным (разделяемым) ресурсам. В общем случае ре- сурс может быть предоставлен либо по чтению, либо по чтению и записи. Для каждого вида доступа указывается свой пароль. Предос- тавить диск или его каталог можно из File Manager, принтер - из Print Manager, а DDE-сервер - из программы Network DDE Share Manager. Последняя программа (DDESHARE.EXE) отсутствует в стандартной поставке Windows for Workgroups; ее можно найти в программном пакете Windows for Workgroups 3.1 Resource Kit или на компакт-диске Microsoft Developer Network CD. Представим, что у нас есть локальная сеть с компьютерами ALPHA и ВЕТА. Название компьютера можно подсмотреть в файле SYSTEM.INI в секции [Network]. Например, для компьютера ALPHA там будет написано: ComputerName=ALPHA Рассмотрим, как запустить программу DDESRVR на компьютере ALPHA, a DDECLNT - на компьютере ВЕТА. Решая эту задачу, по- пытаемся обойтись без Network DDE Share Manager. Ниже приводит- ся последовательность действий, которая позволяет достичь требуе- мого результата в Windows for Workgroups: • поместите программу DDESRVR на компьютеру ALPHA в ката- лог Windows; • на этом же компьютере добавьте в секцию [DDEShares] файла SYSTEM.INI следующую строку: SharedDdeSrvr=ddesrvr,draw„31„0„0,0,0 Перед знаком равенства записывается имя разделяемого DDE-сервера (выбирается произвольно). Следом за знаком равенства идут пара-
380 Программирование в среде Borland Pascal для Windows метры сервера: имя службы, имя темы, имя элемента данных (необязательно), флаги доступа и др. Значение 31 в четвертом пара- метре обеспечивает доступ ко всем возможностям DDE-сервера, включая его автоматический запуск при установке соединения; • измените имена службы и темы в программе DDECLNT. Для на- шего примера имя службы должно быть ’\\ALPHA\NDDE$’, а имя темы - ’SharedDdeSrvr’. В общем случае в качестве имени службы ука- зывается строка \\ComputerName\NDDE$', а в качестве имени темы - имя разделяемого DDE-сервера на удаленном компьютере. Если теперь запустить клиента DDECLNT на компьютере ВЕТА, то на компьютере ALPHA сразу же запустится сервер DDESRVR (на экране появится его пиктограмма). Через DDE-маршрутизатор можно запустить клиента и сервер на одном и том же компьютере. Например, клиент DDECLNT одинаково хорошо работает как на компьютере ВЕТА, так и на компьютере ALPHA. 10.3.9. ВЗАИМОДЕЙСТВИЕ С PROGRAM MANAGER Разработка коммерческих программ обычно заканчивается созданием программы-инсталлятора, предназначенной для установки вашего выдающегося программного продукта на компьютер пользователя. Важный шаг работы инсталлятора - создание в оболочке Program Manager новой группы и добавление в нее маршрутов к программам. Эта операция возможна благодаря тому, что Program Manager под- держивает протокол динамического обмена данными (DDE). С точки зрения DDE, Program Manager - это сервер, а инсталля- тор - клиент. Для соединения с Program Manager клиент должен ука- зать в качестве имени службы и имени темы ’PROGMAN’. После этого он может посылать серверу транзакции, запрашивая информацию о группах и их элементах. Создание новой группы и добавление в нее программ происходит через посылку серверу команд в транзакциях типа xtyp_Execute. Ко- манды представляют собой нуль-терминированные строки. Одна строка может содержать несколько команд. Каждая команда заклю- чается в квадратные скобки, параметры команд заключаются в круг- лые скобки и перечисляются через запятую. Кратко рассмотрим команды, поддерживаемые Program Manager: CreateGroup(GroupName[,GroupPath]) - создает новую группу и активизирует ее. Если группа уже существует, то она просто активи- зируется. GroupName - название новой группы; GroupPath - маршрут к файлу группы. Если маршрут не указан, то Program Manager фор- мирует имя файла из названия группы и помещает файл в каталог Windows; DeleteGroup(GroupName) - удаляет группу с именем Group Name',
Гпава 10. Обмен данными между приложениями 381 ShowGroup(GroupName,ShowCommand) - отображает группу с именем GroupName в состоянии, заданном ShowCommand. Возможные значения ShowCommand: 1 - активизировать и отобразить группу с нормальными координатами и размером; 2 - активизировать и свер- нуть группу в пиктограмму; 3 - активизировать и максимизировать группу; 4 - отобразить (без передачи активности) группу с нормаль- ными координатами и размером; 5 - активизировать и отобразить группу с текущими координатами и размером; 6 - минимизировать группу; 7 - отобразить (без передачи активности) группу как пикто- грамму; 8 - отобразить (без передачи активности) группу с текущими координатами и размером; Reload(GroupName) - обновляет группу с именем GroupName, за- гружая ее из файла группы. Если параметр GroupName не указан, обновляются все группы; AddItem(CmdLine[,ItemName[,IconPath[,IconIndex[,XPos,YPos[,Def Dir [,HotKey[,fMinimize]]]]]]]) - добавляет в группу элемент. CmdLine - полный маршрут к запускаемому файлу; ItemName - название элемен- та в группе; IconPath - путь к файлу, содержащему пиктограмму Эле- мента; Iconindex - номер пиктограммы в файле IconPath; XPos, YPos - позиция пиктограммы в окне группы; DefDir - рабочий каталог; HotKey - “горячая” клавиша; /Minimize - показывает, нужно ли ми- нимизировать окно после запуска; Deleteltem(ItemName) - удаляет элемент с именем ItemName', Replaceltem(ItemName) - удаляет элемент с именем ItemName, за- поминая его позицию. Следующая команда Additem поместит новый элемент в эту позицию; ExitProgman(bSaveGroups) - закрывает окно Program Manager, со- храняя при необходимости (bSaveGroups не равно нулю) информацию о группах. Следующая несложная программа создает в Program Manager группу Example Group и добавляет в нее две стандартные программы: WINFILE.EXE с именем File Manager и CONTROL.EXE с именем Control Panel: program GroupMan; uses WinTypes, Strings, DDEML, WinCrt; { Callback-функция обработки DDE-транзакций } * function DdeCallbackProc(CallType, Fmt: Word; Conv: HConv; HSzl, HSz2: HSz; Data: HDDEData; Datal, Data2: Longint): HDDEData; export; begin DdeCallbackProc := 0; end; {Используя DDE, посылает оболочке Program Manager массив команд} procedure ProgmanExecute(Commands: array of PChar);
382 Программирование в среде Borland Pascal для Windows var Ddelnst: Longint; Service, Topic: HSz; Conv: HConv; I: Integer; begin Ddelnst := 0; if Ddelnitialize(Ddelnst, DdeCallbackProc, AppCmd_ClientOnly, 0) = dmlErr_No_Error then begin Service := DdeCreateStringHandle(Ddelnst, ’PROGMAN’, cp_WinAnsi); Topic := DdeCreateStringHandle(Ddelnst, ’PROGMAN’, cp_WinAnsi); Conv := DdeConnect(Ddelnst, Service, Topic, nil); if DdeGetLastError(Ddelnst) = dmlErr_No_Error then for I := Low(Commands) to High(Commands) do DdeClientTransaction(Commands[l], StrLen(Commands[l]) + 1, Conv, 0, cf_Text, xtyp_Execute, 10000, nil); if Conv <> 0 then DdeDisconnect(Conv); if Service <> 0 then DdeFreeStringHandle(Ddelnst, Service); if Topic <> 0 then DdeFreeStringHandle(Ddelnst, Topic); Ddellninitialize(Ddelnst); end; end; const ' { Команды, посылаемые оболочке Program Manager} Commands: array [1..3] of PChar = ( ’[CreateGroup(Example Group,EXAMPLE)]’ + ’[ShowGroup(Example Group, 1)]’, ’[Addltem(WINFILE.EXE,File Manager)]’, ’[Addltem(CONTROL.EXE,Control Panel)]’); begin ProgmanExecute(Commands); end. В заключение отметим, что на основе DDE построена работа еще более сложного и мощного механизма обмена данными, который называется протоколом привязки и внедрения объектов - OLE (от англ. Object Linking and Embedding). OLE-протокол обеспечивает гибкую интеграцию специализированных программных пакетов, таких как текстовые процессоры, электронные таблицы, базы данных, в единую систему. Он ориентирован на программы работы с документами и позволяет создавать составные документы (compound documents), т.е. документы, содержащие разнородную информацию. Например, с по- мощью OLE можно вставить в документ текстового процессора Word for Windows электронную таблицу, подготовленную в Microsoft Excel. Тема OLE очень обширна (около двухсот функций API) и, к сожале- нию, не может быть рассмотрена в данной книге.
11 ОТЛАДКА ПРОГРАММ В WINDOWS 11.1. ТИПЫ ОШИБОК Разработка любой программы априори предполагает наличие в ис- ходном тексте ошибок и борьбу с ними. Все практически неисчисли- мое множество возможных ошибок обычно подразделяют на три группы: • синтаксические ошибки; • ошибки времени выполнения программы (run-time errors); • смысловые (логические) ошибки. Синтаксические ошибки - это самые простые ошибки, которые лег- ко устраняются уже на этапе компиляции. Причина их одна - непра- вильная запись служебных слов, операторов и т.п. Например, вместо begin написано begim, вместо for - fo и далее в том же духе. Если в ис- ходном тексте программы найдена ошибка, то компиляция прекраща- ется и в строку статуса выводится сообщение, содержащее номер ошибки и ее краткое описание. При этом курсор помещается в то ме- сто, где возникла ошибка. Например, если вы в конце программы после слова end случайно поставили вместо точки (.) точку с запятой (;), компилятор выдаст сообщение: Error 94: expected и установит курсор за словом end. Из текста сообщения не всегда бывает ясна при- чина ошибки. В этом случае выберите в меню Help интегрированной среды команду Error messages и в окне справочной системы отыщите по номеру ошибки ее подробное описание. Обычно устранение син- таксических ошибок не вызывает особых трудностей. Внимание, программисты, использующие кириллицу! Вы можете столкнуться с ситуацией, когда внешне текст выглядит идеально, но компилятор упорно выдает сообщение: Error 5: Syntax error. Одной из причин может быть использование в идентификаторах букв кирилли- цы вместо английских. Например, в слове Count вместо английской буквы ‘С’ (“си”) написана русская буква ‘С’ (“эс”). Чтобы устранить эту ошибку, заново наберите “подозрительный” идентификатор и перекомпилируйте программу. Намного больше неприятностей доставляют ошибки времени вы- полнения. Они дают о себе знать прекращением выполнения програм- мы и сообщением Runtime error, за ним следует код ошибки и адрес, по
384 Программирование в среде Borland Pascal для Windows которому она случилась. Чтобы определить, какой оператор в исход- ном тексте вызвал ошибку, выберите команду меню Search-Find error... и в появившемся окне диалога введите адрес ошибки. При необходи- мости ошибку времени выполнения можно сгенерировать программно с помощью процедуры RunError. Это бывает удобно во время отлад- ки, однако помните, что коммерческая программа не должна шокиро- вать пользователя сообщениями типа Runtime error. Чаще всего ошиб- ка времени выполнения является симптомом смысловой ошибки. Смысловые (логические) ошибки - самые сложные и трудноулови- мые. Они проявляются в том, что программа ведет себя не совсем так, как хотелось бы. Последствия смысловых ошибок могут быть самыми разными: безобидными - неправильное содержимое окна, невыполне- ние или неверное выполнение команд пользователя, неправильное содержимое выходных файлов, а также достаточно серьезными - про- грамма может “свалиться” или досрочно завершиться с ошибкой вре- мени выполнения, может произойти выход из Windows в командную строку MS-DOS и многое-многое другое. На борьбу со смысловыми ошибками уходит почти все время отладки. 11.2. ПРОЦЕСС ОТЛАДКИ ПРОГРАММЫ 11.2.1. ЭТАПЫ ОТЛАДКИ Отладка программы является итеративным процессом обнаружения и исправления ошибок, который обычно требует последовательного выполнения четырех этапов: • выявление ошибки; • локализация ошибки в тексте программы; • установление причины ошибки; • исправление ошибки. В конкретных ситуациях перечисленные этапы могут пересекаться, некоторых из них может и не быть, однако в общем случае дело об- стоит именно так. 11.2.2. ВЫЯВЛЕНИЕ ОШИБКИ Выявление ошибок иначе называют тестированием. Некоторые ошибки проявляются после первого же запуска программы на выпол- нение. Они заметны “невооруженным глазом”, и для их обнаружения не надо прибегать ни к каким специальным средствам. Например, сразу можно заметить “заползание” комментирующего текста на ото- бражаемый программой рисунок. Некоторые ошибки проявляются в чисто случайные моменты ра- боты программы. С такими ошибками справиться труднее всего. Если
Глава 11. Отладка программ в Windows 385 вы не можете зафиксировать условия возникновения ошибки, вы не сможете понять причину ошибки и устранить ее. Иногда, чтобы до- биться устойчивого проявления ошибки, перед каждым стартом про- граммы приходится заново перезагружать компьютер. Существует класс ошибок, связанных с недостаточно аккуратным использованием ресурсов компьютера. Например, при выделении памяти некоторые программисты ошибочно полагают, что система всегда удовлетворяет этот запрос. Такие ошибки остаются скрытыми в нормальных условиях, но могут проявиться при увеличении загруз- ки системных ресурсов. Для выявления этих ошибок в Windows суще- ствует библиотека STRESS.DLL. Она содержит функции, которые позволяют создать неблагоприятные условия для работы программы: ограниченный объем свободной памяти, отсутствие свободных деск- рипторов файлов, недостаток других системных ресурсов. Программисты говорят, что каждая найденная в программе ошиб- ка является предпоследней. Из этого следует, что процесс тестирова- ния может продолжаться бесконечно. Однако рано или поздно про- грамма должна выйти в свет. Поэтому производители программного обеспечения применяют так называемое бета-тестирование. До вы- пуска коммерческой версии программа появляется в виде бета-версии и предлагается потенциальным пользователям для предварительной эксплуатации и тестирования. При этом срабатывает известный принцип, что лучше всего ошибки находит тот, кто не причастен к разработке программы. Ошибки, выявленные на этапе бета- тестирования, фиксируются и устраняются в коммерческой версии продукта. Еще одним положительным моментом бета-тестирования является то, что оно позволяет оценить программу с точки зрения удобства ее использования, собрать критические замечания пользова- телей и учесть их в конечном варианте программы. 11.2.3. ЛОКАЛИЗАЦИЯ ОШИБКИ В ТЕКСТЕ ПРОГРАММЫ После того, как ошибка зафиксирована, необходимо найти в исход- ном тексте программы то место, в котором она возникает. Другими словами, нужно локализировать ошибку. Первое, что приходит в голо- ву, - протрассировать программу по шагам с помощью отладчика. Однако сделать это далеко не всегда возможно, особенно в больших и сложных программах. Если же учесть, что программы в Windows имеют событийно-управляемую архитектуру, то становится ясно, что выполнить полную трассировку всей программы вряд ли возможно. Для поиска ошибок в Windows-приложениях используют точки останова с трассировкой только отдельных участков. Точки останова устанавливаются на методы обработки сообщений оконных объектов и позволяют отследить наступление интересующих программиста событий. Правильность обработки сообщения Windows проверяется с 13 Зак. 1049
386 Программирование в среде Borland Pascal для Windows помощью трассировки соответствующего метода ответа на сообщение после прерывания программы в точке останова. Сначала трассирова- ние осуществляется без захода в вызываемые процедуры и функции. Если до вызова некоторой процедуры ошибки не было, а после ее вызова наблюдаются признаки ошибки, то, очевидно, следует отла- дить эту процедуру. Для ускорения локализации ошибки используется также метод упрощения логики работы программы. Комментируйте, взяв в фигур- ные скобки, те участки программы, которые могут быть причастны к ошибке, лишив ее некоторых возможностей. Запустите программу и убедитесь, что в усеченном варианте она работоспособна. Постепенно убирайте комментарии, усложняя поведение программы и проверяя ее работу на каждом шаге. На одном из таких шагов ошибка себя про- явит. В последнем добавленном фрагменте программы и следует ис- кать ошибку. Иногда просто требуется проверить, проходит ли программа через определенную точку. Быстрее всего это можно сделать, если вставить в этой точке вызов Windows-функции MessageBeep, которая генериру- ет звуковой сигнал. А если подключить модуль WINCRT, то можно использовать процедуры Write и Writein для выдачи отладочной ин- формации в дополнительное окно, создаваемое этим модулем. 11.2.4. УСТАНОВЛЕНИЕ ПРИЧИНЫ ОШИБКИ В общем случае причина ошибки может быть связана с недопонима- нием задачи, недостатками проектирования, ошибками, допущенны- ми при кодировании. Недопонимание проблемной области приводит к самым тяжелым и трудноустранимым ошибкам - ошибкам спецификации. Помните, что чем больше времени потрачено на разработку спецификации и проек- тирование, тем меньше времени будет потрачено на ее отладку. Если программа хорошо спроектирована, то запрограммировать ее, как правило, не составляет особого труда. При этом возрастает ее надеж- ность, т.е. уменьшается вероятность обнаружения ошибки. Самые распространенные ошибки - это ошибки кодирования. Среди них наиболее часто встречаются следующие: использование перемен- ных (в особенности указателей) без предварительной инициализации, выход за границы диапазона при работе со строками и массивами, неверный расчет числа итераций цикла, переполнение разрядной сет- ки при выполнении арифметических операций, накопление погрешно- сти вычислений в операциях с вещественными числами. Некоторые ошибки специфичны для Windows, которая игнорирует вызовы функций с неправильными значениями параметров. Поэтому, например, если в окне не рисуется изображение, то, скорее всего, это результат передачи нулевых дескрипторов при вызове функций GDI.
Глава 11. Отладка программ в Windows 387 Следующий тип ошибок связан с особенностями защищенного ре- жима процессора, в котором работает Windows. В этом режиме обес- печивается аппаратная защита кода и данных каждой программы от их изменения другими программами. Если некоторая область памяти не принадлежит данному приложению или не является разделяемой, то любое обращение к ней приводит к нарушению общей защиты па- мяти (от англ. General Protection Fault - GPF). Приложение, которое пытается нарушить целостность другого приложения, немедленно уничтожается системой Windows. При этом на экран выдается сооб- щение, поясняющее, в каком приложении и по какому адресу про- изошла ошибка GPF. Остальные приложения продолжают нормаль- ную работу. Очевидно, что Windows умеет справляться с ситуациями, в которых MS-DOS просто бессильна. Ошибки в динамически подключаемых библиотеках имеют свои особенности. Если в DLL-библиотеке происходит критическая ошиб- ка, то Windows считает виновником ошибки не DLL-библиотеку, а приложение, которое к ней обратилось. В результате приложение бу- дет закрыто, но сама динамическая библиотека останется загружен- ной. Даже если вы исправите ошибку и перекомпилируете DLL- библиотеку, Windows не загрузит ее до тех пор, пока в памяти остает- ся ее предыдущая версия. Единственный выход из этой ситуации - перезапуск Windows. 11.2.5. ИСПРАВЛЕНИЕ ОШИБКИ При исправлении ошибки самое главное - не внести в программу но- вых ошибок. При выборе алгоритма жертвуйте скоростью ради на- дежности! Не стоит сразу писать критические по скорости процедуры и функции на Ассемблере, так как отладка и переделка ассемблерных подпрограмм намного сложнее, чем написанных на языке высокого уровня. 11.3. ОТЛАДЧИК TURBO DEBUGGER ДЛЯ WINDOWS 11.3.1. ОБЩИЕ СВЕДЕНИЯ Интегрированная среда BPW не содержит встроенных средств отлад- ки. Для этой цели следует использовать автономный отладчик Turbo Debugger для Windows (TDW), который является составной частью программного пакета. TDW можно легко вызвать из среды BPW, вы- брав в меню Tools команду Turbo Debugger или нажав соответствую- щую кнопку инструментальной панели. При этом программа будет откомпилирована с нужными для отладки директивами компилятора, а потом будет запущен TDW.
388 Программирование в среде Borland Pascal для Windows Turbo Debugger для Windows представляет собой приложение Windows, работающее в текстовом режиме. Среда Turbo Debugger для Windows мало чем отличается от среды Turbo Debugger для DOS, по- этому, если вы уже умеете отлаживать программы в DOS, отладка программ в Windows не будет для вас проблемой. Отладчик TDW является особым Windows-приложением: находясь в его среде, вы не можете использовать диспетчер задач (Task Manager) или системные клавиши Ctrl+Esc и Ait+Esc для переключе- ния между приложениями. Ограничение другого рода, которое скорее относится к числу недоработок, состоит в том, что на некоторых SVGA-адаптерах отладчик не всегда может правильно восстановить содержимое экрана при работе в видеорежиме с высоким разрешением (более 640x480 точек 16 цветов). Существует два выхода из этой си- туации: можно отлаживать программу в стандартном видеорежиме адаптера VGA (640x480 точек 16 цветов) или использовать для отлад- ки дополнительный монитор (обычно монохромный). В последнем случае окно отлаживаемой программы отображается на первом мони- торе, а среда отладчика - на втором. Программа TDW обладает мощным оконным интерфейсом. Для работы с окнами используются приблизительно те же приемы, что и при работе с интегрированной средой Borland Pascal для Windows (естественно, с поправкой на текстовый режим). Управление работой отладчика выполняется через систему меню или с помощью “горячих” клавиш. В результате запуска Turbo Debugger из интегрированной среды BPW в отладчик загружается отлаживаемая программа и ее точка входа отмечается стрелкой (рис. 11.1). Теперь можно приступать непо- средственно к отладке программы. 11.3.2. УПРАВЛЕНИЕ ОТЛАДКОЙ Управлением ходом выполнения программы осуществляется с помощью команд из меню Run. Если выбирается команда Run (“горячая” клавиша F9), выполнение программы продолжается до тех пор, пока не достигнут ее конец, либо до первой точки останова. Вы- полнение программы может быть прервано нажатием клавиш Ctrl+Alt+SysRq. Команда Go to cursor (“горячая” клавиша F4) позволяет ускорить процесс отладки. Программа выполняется до строки, в которой в данный момент располагается текстовый курсор. Например, если про- грамма попадает в цикл, то, чтобы не выполнять все его итерации по шагам, поместите курсор на следующую строку после тела цикла и нажмите F4. Точно так же можно пропустить ту часть отлаживаемой программы, детальный анализ которой не требуется.
Глава 11. Отладка программ в Windows 389 Рис. 11.1. Отладчик Turbo Debugger для Windows Команда Trace into (“горячая” клавиша F7) вызывает выполнение кода, соответствующего одной строке текста программы. Если в строке записан вызов подпрограммы, начинается трассировка по тек- сту подпрограммы (библиотечные процедуры и функции выполняют- ся без трассировки за одно нажатие F7). При необходимости выпол- нить строку программы за один шаг, без захода в вызываемые под- программы, используется команда Step over (“горячая” клавиша F8). Команда Execute to... позволяет задать адрес инструкции, по дос- тижении которой выполнение программы приостанавливается. Команда Until return продолжает выполнение подпрограммы без остановки до тех пор, пока не происходит возврат в точку вызова. Команда очень удобна в тех случаях, когда по забывчивости вместо клавиши F8 (Step over) нажимается F7 (Trace Into) и начинается трас- сировка какой-то другой подпрограммы. Команда удобна для поша- гового исследования поведения процедуры или функции в самом ее начале и “быстрого” завершения выполнения при правильной работе кода. С помощью команды Animate... отладчик предоставляет возмож- ность “анимации”, когда исполнение программы идет по шагам и машинные команды выполняются с заданной скоростью. Из других команд управления выполнением отметим команду Back trace (“горячие” клавиши Alt+F4). Она позволяет “вернуться” назад по коду отлаживаемой программы, правда, с некоторыми ограниче- ниями. Например, нельзя “вернуть” назад связанную с портами инет-
390 Программирование в среде Borland Pascal для Windows рукцию. Движение назад возможно потому, что отладчик хранит по- следние выполненные инструкции. Просмотреть их можно, активизи- ровав окно Execution History через меню View. Команда Instruction trace (Ait+F7) позволяет в любой момент пе- рейти от отладки программы на уровне исходного текста к отладке на уровне машинных инструкций. Если необходимо начать выполнение программы с начала, не до- жидаясь ее завершения, используется команда Program reset (“горячие” клавиши Ctri+F2). 11.3.3. ТОЧКИ ОСТАНОВА Важнейшим средством управления отладкой являются точки останова (breakpoints). Включение точки останова выполняет команда Toggle меню Breakpoints (“горячая” клавиша F2). Точка помещается в той строке программы, где располагается текстовый курсор. Повторное выполнение команды Toggle удаляет точку останова. Строка про- граммы, на которую установлена точка останова, высвечивается на экране другим цветом. Когда процесс выполнения программы достигнет точки останова, отладчик прервет ее выполнение и пользователь получит возможность просмотреть данные и код, установить другие точки останова, а также продолжать выполнение программы по шагам (клавиши F7 и F8) до следующей точки останова (клавиша F9) с помощью описанных выше способов. Так работают самые простые, безусловные, точки останова. В действительности возможности точек останова в Turbo Debugger гораздо шире, чем обычная остановка программы. Точки останова в Turbo Debugger характеризуются следующими параметрами: • состояние активности; • размещение; • условие; • число проходов; • действие; • группа принадлежности. Чтобы определить новую точку останова и сразу задать все эти пара- метры, выберите в меню Breakpoints команду At.... Состояние активности. Точка останова может быть либо включе- на, либо выключена. Очевидно, что отладчик контролирует только те точки останова, которые в данный момент включены, и игнорирует выключенные. Размещение. При размещении точки останова вы можете либо “привязать” ее к конкретной строке исходного текста, либо сделать глобальной для всей программы. Если задана глобальная точка оста-
Глава 11. Отладка программ в Windows 391 нова, ее условие проверяется после выполнения каждой строки про- граммы. Наличие глобальных точек останова существенно замедляет выполнение программы, поэтому их лучше отключить, пока выполня- ется код, не требующий отладки. Условие. С точкой останова может быть связано условие (такие точки останова называют условными). Условная точка останова по- зволяет программе выполняться без остановок до того момента, пока условие не станет истинным. Условие задается как выражение, резуль- тат которого должен иметь тип Boolean. Вы можете задать несколько условий; остановка программы произойдет, только когда все условия станут истинными. Представьте, например, что некоторая функция вашей программы работает неправильно при отрицательных значе- ниях своих аргументов. На эту функцию можно установить безуслов- ную точку останова, но тогда вы устанете нажимать клавишу F9 в ожидании отрицательных значений параметров функции. Лучше за- дать условную точку останова, возложив эту работу на отладчик. Одним из видов условий является изменение памяти. Если задать адрес и размер некоторой области памяти, то при ее изменении вы- полнение программы будет прервано. Допустим, вы обнаружили, что некоторая переменная в программе принимает некорректные значе- ния. Глобальная точка останова изменения памяти, поможет обнару- жить, в каких частях программы происходит изменение этой перемен- ной. Для глобальных точек останова Turbo Debugger поддерживает не- сколько уровней аппаратной отладки. Это возможно только для ком- пьютеров с процессором 80386 и выше, при этом в файле SYSTEM.INI в секции [386Enh] должен быть подключен драйвер TDDEBUG.386, например: device=tddebug.386. Обычно это делает за вас программа инсталляции. При использовании TDDEBUG.386 контроль за опера- циями получает аппаратную поддержку. Условия для аппаратных точек останова могут включать чтение-запись оперативной памяти, ввод-вывод через порты и др. При программировании на Borland Pascal эту возможность используют редко. Число проходов. Очень полезен параметр, задающий число прохо- дов. Если в цикле 100 итераций и ошибка возникает на сотый раз, то нет нужды выполнять по шагам все 100 итераций, лучше просто уста- новить в теле цикла точку останова с числом проходов 100. Отметим, что для точки останова можно одновременно задать й условие, и чис- ло проходов. В этом случае учитываются только те проходы, во время которых условие истинно. Действие. Прерывание выполнения программы с передачей управ- ления отладчику - не единственный способ срабатывания точки оста- нова. Кроме этого, можно указать и другие действия: выполнение некоторого выражения (что фактически означает “вплетение” в код
392 Программирование в среде Borland Pascal для Windows программы дополнительных операторов), запись некоторой инфор- мации в журнал, включение или выключение других точек останова, принадлежащих заданной группе. Группа принадлежности. Для каждой точки останова указывается идентификатор группы, к которой она принадлежит. Точки останова, принадлежащие одной и той же группе, могут одновременно вклю- чаться или выключаться в результате срабатывания других точек ос- танова. 11.3.4. ОКНА ОТЛАДЧИКА В TDW существует большое количество типов окон, которые созда- ются с помощью команд меню View и обеспечивают разные способы просмотра кода, данных и других областей памяти. Каждое окно имеет собственное меню команд, которое вызывается по нажатии клавиш Alt+Fl 0. Команды локального меню имеют свои “горячие” клавиши, образуемые комбинацией клавиши Ctrl и высве- ченной буквы в названии команды. Нажатие Ctrl изменяет строку статуса, давая оперативную подсказку о “горячих” клавишах, дейст- вующих для данного окна. Рассмотрим назначение основных окон отладчика TDW: • - • окно Breakpoints содержит список всех точек останова и обеспе- чивает управление ими. Вы можете добавить новую точку останова (команда Add... локального меню), изменить параметры существую- щей (Set options...), удалить выбранную точку останова (Remove). Ко- мандой Inspect можно отыскать в исходном тексте программы вы- бранную точку останова и показать ее в окне Module; • в окне Stack отображаются активные в данный момент процеду- ры и функции вместе со значениями их параметров. Просматривая окно Stack, вы можете определить, какая цепочка вызовов процедур и функций привела к текущему месту программы. Чтобы просмотреть список локальных переменных определенной процедуры или функции, нужно выбрать в локальном меню команду Locals. С помощью ко- манды Inspect вы можете открыть окно просмотра, в котором отобра- зится исходный текст выбранной подпрограммы; • окно Log - это журнал, в который можно помещать любую инте- ресующую информацию. По умолчанию окно Log рассчитано на 50 строк, но с помощью TDWINST.EXE это число можно увеличить до 200. Если журнал полностью заполнен, то добавление каждой новой строки приводит к выталкиванию первой. Имеется возможность свя- зать окно Log с файлом на диске (команда Open log file... локального меню). После этого информация, помещаемая в журнал, будет авто- матически сохраняться в указанном файле. Если в некоторой точке программы происходит сбой, то информация, сохраненная в файле,
Глава 11. Отладка программ е Windows 393 может быть использована для анализа его причины. Команда Display Windows info... позволяет выдать в окно Log содержимое глобальной или локальной динамической памяти, а также список всех исполняю- щихся задач и DLL. Вы также можете вручную добавить в конец жур- нала любые комментарии (для этого служит команда Add comment...); • окно Watches используется для наблюдения за значениями пере- менных и выражений в ходе исполнения программы. С помощью ко- манд локального меню можно добавить в окно Watches новую пере- менную или выражение (команда Watch...), заменить одно выражение на другое (команда Edit...), удалить выбранную строку (команда Remove). Можно даже изменить значение переменной, выбрав в ло- кальном меню команду Change...; • в окне Variables отображаются все доступные программе пере- менные. В верхней части окна помещаются все глобальные перемен- ные, а в нижней части - все локальные переменные активной в данный момент процедуры или функции. Выделенную в окне переменную можно проинспектировать (команда Inspect) или поместить в окно Watches (команда Watch). Команда Change... локального меню позво- ляет изменить ее значение; • окно Module содержит исходный текст отлаживаемой програм- мы. Оно открывается автоматически при запуске отладчика TDW из интегрированной среды Borland Pascal. Это основное окно, в котором выполняется большая часть работы по отладке программы. Следую- щая исполняемая строка в окне Module отмечается символом стрелки слева от текста. Переменные йли выражения, содержащиеся в тексте программы, можно проинспектировать или добавить в окно Watches (для этого служат команды Inspect и Watch... локального меню). С помощью команды Module... можно выбрать и загрузить в окно Module другую программу, модуль или DLL. С помощью команды File... локального меню можно выбрать и загрузить в окно Module другой модуль (если программа имеет многомодульную структуру) или файл (если модуль состоит из нескольких файлов). Очень полез- ной является команда Search..., которая осуществляет поиск заданной строки в исходном тексте. Поиск начинается от текущего положения текстового курсора и продолжается вниз до первого совпадения. Ко- манда Next позволяет продолжить поиск строки, начатый командой Search.... Если вы потеряли то место, где остановилась программа, то команда Origin поможет быстро его найти. Команда Line... быстро переместит окно просмотра на строку с указанным номером, а коман- да Goto... - на указанный адрес; • окно CPU содержит информацию, необходимую для наблюдения за процессом исполнения программы на уровне машинного кода и оперативного вмешательства - корректировки регистров и флагов, изменения содержимого сегмента данных и стека. Оно состоит из шее-
394 Программирование в среде Borland Pascal для Windows ти подокон, называемых панелями. В левом верхнем углу панель ма- шинного кода содержит деассемблированные инструкции программы вместе с Pascal-строками, эквивалентом которых они являются. Спра- ва к ней примыкает панель регистров, показывающая текущее значе- ние регистров процессора, а снизу - панель селекторов, которая со- держит информацию о селекторах адресов памяти. Справа вверху располагается панель флагов процессора. Слева внизу находится панель данных, в которой отображается дамп памяти, соответствующий сег- менту данных или выбранному селектору. В правом нижнем углу по- мещается панель стека, где дается дамп текущей его вершины. Одна из панелей является активной (та, в которой размещен курсор). Переход от одной панели к другой осуществляется клавишей Tab (Shift+Tab) или указателем мыши. Информация в панелях соответствует текущему состоянию процессора и обновляется после каждой выполненной ма- шинной команды. Каждая из панелей имеет свое локальное меню. Панель машинного кода позволяет наблюдать за процессом испол- нения отлаживаемой программы на уровне машинных инструкций. Следующая исполняемая инструкция (инструкция-курсор) обознача- ется стрелкой. Локальное меню панели содержит команды: Goto..., Origin, Follow, Caller, Previous, Search, View source, New cs:ip, Assemble..., I/O. Далее предельно кратко рассмотрены лишь некоторые из них. Выбор команды Goto позволяет переместить просмотр на указан- ный адрес. Он задается в формате $nnnn, например $ 10С4:$0010. При этом положение инструкции-курсора не изменяется. Выполнение ко- манды Goto cursor меню Run (клавиша F4) вызывает выполнение про- граммы до новой границы. Команда Origin локального меню возвра- щает панель машинного кода к текущей инструкции-курсору. Коман- да Follow позволяет “заглянуть” на одну инструкцию вперед. Она ото- бражает в панели машинный код со следующей инструкции, куда бу- дет передано управление, если инструкция-курсор будет выполнена. Команда Search позволяет задать машинную команду в виде ассемб- лерной инструкции или цепочки байт, которая будет отыскиваться в текущем кодовом сегменте программы. Если она найдена, в панель будет помещен дезассемблированный фрагмент программы, начиная с найденной строки. Следует иметь в виду, что при ошибочном выборе границы возможно некорректное дезассемблирование кода. Команда View source загружает в окно Module исходный текст на языке Pascal, соответствующий исполняемым машинным инструкциям в окне CPU. Команда локального меню New cs:ip позволяет, изменив значения регистров CS и IP, исследовать любой фрагмент кода. И, наконец, команда I/O открывает подменю для выполнения машинных команд доступа к портам компьютера, которые позволяют прочесть (записать) байт или слово из порта (в порт).
Глава 11. Отладка программ в Windows 395 Панели регистров и флагов позволяют изменять значения регистров или флагов. Задание нового значения регистра требует активизации панели и высвечивания его символического обозначения. Локальное меню панели регистров содержит команды для “избирательного” из- менения регистра: Increment увеличивает, a Decrement уменьшает зна- чение высвеченного регистра на 1; Zero сбрасывает регистр в нуль; Change открывает окно для ввода произвольного числа. Изменение флагов выполняется проще. Нужный флаг подсвечивается и нажима- ется клавиша Enter (что эквивалентно выбору команды локального меню Toggle). Каждое нажатие Enter изменяет текущее значение флага на противоположное. Значение 0 соответствует сброшенному, а 1 - установленному флагу. Перечислим флаги в панели: с - флаг переноса (Carry Flag); z - флаг нуля (Zero); s - флаг знака (Sign); о - флаг пере- полнения (Overflow); р - флаг четности (Parity); а - флаг дополнитель- ного переноса (Auxiliary); i - флаг прерываний (Interrupt); d - флаг на- правления (Direction). Панель стека позволяет записать в стек новые значения. Сделав эту панель активной и выбрав нужную строку, начинайте цифровой ввод. Поведение отладчика аналогично рассмотренному для регистров. В защищённом режиме, в котором работает Windows, адреса фор- мируются из селектора и смещения. Селектор представляет собой индекс в специальной системной таблице, в которой хранится инфор- мация о сегментной части адреса, правах доступа, размере отведенно- го для программы блока памяти и др. При любом обращении к памя- ти происходит аппаратная трансляция логического адреса в виде [селекторхмещение] в физический [сегментхмещение], выдаваемый на шину адреса, с одновременной проверкой прав доступа. В панели се- лекторов просто отображается содержимое таблицы селекторов. Если вы отметите соответствующий селектор и выберете в локальном меню команду Examine, то сможете просмотреть содержимое любого блока памяти как вашей программы, так и любой другой. Если это селектор кода, данные будут загружены в панель кода, а если селектор дан- ных, - то в панель данных. В нижней части окна CPU находится панель данных. В нее помеща- ется текущий дамп сегмента данных. Активизировав эту панель, мож- но просмотреть и изменить содержимое любого сегмента памяти. От- метим, что нельзя просмотреть область памяти, не зная селектора, по которому к ней осуществляется доступ. Единственный способ для просмотра области памяти, не относящейся к вашей программе, - найти ее селектор в панели селекторов. Локальное меню для панели данных включает такие команды: Goto..., Search..., Next, Change..., Follow, Previous, Display as, Block. Команда Display as управляет формой представления информации и предлагает выбор из собственного подменю: Byte отображает дамп
396 Программирование в среде Borland Pascal для Windows побайтно, Word - по словам, Long - по группам из четырех байт и т.п. Команда Block позволяет манипулировать целыми блоками памяти. Блок задается началом и длиной. Используя подменю работы с бло- ками, можно: обнулить блок памяти (Clear), переместить блок на дру- гое место (Move), установить все байты блока в одно и то же значение (Set), прочесть блок с диска и разместить его в памяти (Read), запи- сать блок памяти в файл на диске (Write). Команда Goto... выводит в панель дамп памяти с новой границы. Команда Search удобна для поиска позиции в дампе, содержащей заданную строку байт. Повто- рение поиска выполняет команда Next. Команда Change... открывает окно, позволяющее задать новое значение байт памяти. Те же дейст- вия можно выполнить и так: высветить в панели данных нужный байт или выбрать байт курсором; затем начать ввод нового значения. От- ладчик открывает такое же окно ввода, как и при выполнении коман- ды Change...; • окно Regs, открываемое по команде меню View-Registers, и ко- манды его локального меню совпадают с панелями регистров и фла- гов окна CPU, речь о которых шла выше; • окно Dump, открываемое по команде меню View-Dump, подобно рассмотренной ранее панели данных окна CPU. Окна Regs и Dump следует использовать тогда, когда не требуется вся информация о процессоре; • окно Hierarchy открывается по команде главного меню View- Hierarchy. Оно служит для представления иерархии объектов в объ- ектно-ориентированных программах, в частности в приложениях на OWL. В левой панели содержится список доступных в текущем модуле объектных типов, который упорядочен по алфавиту, а в правой - де- рево этих же объектов. Выбрав в локальном меню любой из двух па- нелей команду Inspect, вы можете получить информацию о всех полях и методах выделенного объекта; • окно Windows messages служит для работы с сообщениями. Windows-приложения имеют событийно-управляемую архитектуру. Сообщения Windows постоянно “протекают” через тело программы, управляя последовательностью вызова процедур, функций и методов объектов. В связи с этим TDW предоставляет специальные средства фильтрации сообщений Windows, адресованных тому или иному окну. Эти средства становятся доступными после открытия окна Windows messages с помощью команды главного меню View-Windows messages. В этом окне указывается, для какого окна или окон требуется про- сматривать сообщения. При появлении сообщения заданного типа TDW может либо прервать его обработку, либо зарегистрировать его появление. В некотором смысле это еще один тип точки останова. Окно Windows messages образовано тремя панелями. Левая верхняя панель содержит список оконных процедур и дескрипторов окон, за
Гпава 11. Отладка программ в Windows 397 сообщениями которых происходит наблюдение. Список анализируе- мых сообщений содержится в правой верхней панели. Нижняя панель представляет собой журнал, в который заносится информация об “отловленных” сообщениях. Каждая из панелей имеет свое локальное меню. При отладке программ, написанных на OWL, обычно выбирается команда Add... в локальном меню левой верхней панели и в открыв- шемся диалоговом окне вводится дескриптор интересующего про- граммиста окна. Например, для главного окна можно ввести: Application74.MainWindowA.HWindow. При этом в группе зависимых кнопок Identify by следует выбрать Handle для ввода дескриптора ок- на. Помните, что дескриптор окна становится действительным только после выполнения метода SetupWindow оконного объекта. По умолчанию выбирается режим регистрации всех сообщений (об этом свидетельствует запись Log all messages в панели справа вверху). С помощью команды Add... этой панели можно “заказать” регистра- цию или прерывание программы только по одному или нескольким классам сообщений. Основная проблема, с которой обычно сталкива- ется программист при отслеживании сообщений, - это огромное их количество,, поэтому “фильтр” следует устанавливать на как можно меньшее число классов сообщений. о
ПРИЛОЖЕНИЯ А. МОДУЛИ BORLAND PASCAL ДЛЯ WINDOWS Модуль Описание Стандартные модули SYSTEM “Сердце” Borland Pascal, содержащиеся в нем под- программы обеспечивают работу всех остальных модулей системы. Подключается автоматически OBJECTS Содержит стандартные объекты Borland Pascal и используется при объектно-ориентированном про- граммировании STRINGS Содержит набор процедур и функций для работы с нуль-терминированными строками языка С VALIDATE Содержит набор объектов для проверки допустимо- сти вводимых данных WINCRT Служит для быстрой разработки простых Windows- программ, не требующих развитого интерфейса. Обеспечивает эмуляцию текстового режима DOS в графическом окне Windows с возможностью исполь- зования стандартных процедур ввода-вывода Read, Readln, Write и Writein WINDOS Предоставляет доступ к функциям DOS, в частности к функциям работы с диском WINPRN Предоставляет упрощенный доступ к печатающему устройству как к текстовому файлу Модули Windows API WINTYPES Включает определения всех констант и типов данных, используемых функциями Windows API WINPROCS Предоставляет доступ к стандартным функциям Windows API. Модуль WinProcs вместе с модулем WinTypes составляет версию Windows API для Borland Pascal WIN31 Обеспечивает поддержку дополнительных возможно- стей API для Windows 3.1 COMMDLG Набор стандартных окон диалога для выбора цвета, шрифта, открытия и сохранения файла, управления принтером, выполнения операций поиска и замены DDEML Высокоуровневый интерфейс для динамического обмена данными между программами (DDE)
Приложения 399 Окончание прил. А Модули Описание LZEXPAND Функции распаковки файлов, упакованных по алго- ритму Lempel-Ziv MMSYSTEM OLE Предоставляет доступ к средствам мультимедиа Высокоуровневый интерфейс для “привязки и вне- дрения объектов” (OLE) SHELLAPI Позволяет строить операционные оболочки, подоб- ные Program Manager, и предоставляет доступ к тех- нологии “перетяни и оставь” (drag-and-drop) STRESS Функции из этого модуля служат для проверки рабо- тоспособности программы в неблагоприятных усло- виях (стрессовых ситуациях). Они намеренно “пожирают” память, дескрипторы файлов и другие системные ресурсы TOOLHELP VER Содержит функции, предназначенные для отладки Используется при написании программ-инсталля- торов. Позволяет устанавливать программы новых версий на место используемых старых WINMEM32 Функции 32-разрядного управления памятью Модули библиотеки OWL OMEMORY Обеспечивает гибкое управление динамической памятью OWINDOWS Содержит базовые объекты OWL: TApplication, TWindowsObject, TWindow, TMDIWindow, TMDIClient, TScroller ODIALOGS Содержит диалоговые и управляющие объекты OWL: TDialog, TDlgWindow, TControl, TGroupBox, TButton, TCheckBox, TRadioButton, TStatic, TEdit, TListBox, TComboBox, TScrollBar OSTDDLGS Диалоговые объекты TFileDialog и TInputDialog этого модуля служат для организации выбора файла и ввода строки в окне диалога OSTDWNDS Содержит оконные объекты TEditWindow и TFileWindow, обеспечивающие редактирование тек- ста в окне OPRINTER Предоставляет объекты для организации печати: TPrinter, TPrintOut, TEditPrintout, TWindowPrintout, TPrintDialog, TPrinterSetupDlg, TPrinterAbortDlg BWCC Обеспечивает использование управляющих элемен- тов фирмы Borland International (Borland Windows Custom Controls)
400 Программирование в среде Borland Pascal для Windows Б. СИСТЕМНЫЕ МЕТРИКИ WINDOWS Константа Описание sm_CXScreen sm_CYScreen sm_CXVScroll Ширина экрана Высота экрана Ширина кнопки со стрелкой для вертикальной полосы скроллинга sm_CYVScroll Высота кнопки со стрелкой для вертикальной полосы скроллинга sm_CXHScroll Ширина кнопки со стрелкой для горизонтальной полосы скроллинга sm_CYHScroll Высота кнопки со стрелкой для горизонтальной полосы скроллинга sm_CYCaption Высота заголовка окна с учетом неизменяемой в размерах рамки (sm_CYBorder) sm_CXBorder sm_CYBorder sm_CXDlgFrame sm_CYDlgFrame sm_CYVThumb Ширина неизменяемой в размерах рамки Высота неизменяемой в размерах рамки Ширина рамки для окна со стилем ws_DlgFrame Высота рамки для окна со стилем ws_DlgFrame Ширина бегунка для вертикальной полосы скрол- линга sm_CXHThumb Высота бегунка для горизонтальной полосы скроллинга sm_CXIcon sm_CYIcon sm_CXCursor sm_CYCursor sm_CYMenu Ширина пиктограммы Высота пиктограммы Ширина курсора мыши Высота курсора мыши Высота однострочной полосы меню без учета толщины неизменяемой в размерах рамки (sm_CYBorder) sm_CXFullScreen Ширина рабочей области максимизированного окна sm_CYFullScreen Высота рабочей области максимизированного окна sm_CYKanjiWindow' sm_MousePresent sm_Debug Высота окна Kanji Не равно нулю, если в системе присутствует мышь Не равно нулю, если версия Windows является отладочной sm_SwapButton Не равно нулю, если левая и правая кнопки мыши “переназначены” друг на друга sm_CXMin Минимальная ширина окна
Приложения 401 Окончание прил. Б Константа Описание sm_CYMin sm_CXSize Минимальная высота окна Ширина растровых изображений, содержащихся в заголовке окна sm_C YSize Высота растровых изображений, содержащихся в заголовке окна sm_CXFrame sm_CYFrame sm_CXMinTrack sm_C YMinT rack sm_CXDoubleClk Ширина изменяемой в размерах рамки Высота изменяемой в размерах рамки Минимальная ширина, переносимого окна Минимальная высота переносимого окна Ширина прямоугольника, в пределах которого два щелчка мыши распознаются как двухкратный щелчок sm_CYDoubleClk Высота прямоугольника, в пределах которого два щелчка мыши распознаются как двухкратный щелчок sm_CXIconSpacing Ширина прямоугольников, используемых для расположения неперекрывающихся пиктограмм sm_CYIconSpacing Высота прямоугольников, используемых для рас- положении неперекрывающихся пиктограмм sm_MenuDropAlignment Способ выравнивания всплывающих (pop-up) меню. Если значение метрики равно нулю, то левая сторона всплывающего меню выравнивает- ся по левой стороне соответствующего элемента полосы меню. Если же значение метрики не равно нулю, то левая сторона всплывающего меню вы- равнивается по правой стороне соответствующего элемента полосы меню sm_PenWindows sm_DBCSEnabled Дескриптор DLL-библиотеки для Pen Windows Не равно нулю, если в текущей версии Windows используется двухбайтное кодирование симво- лов - DBCS (Double-Byte Character Set) В. ВИРТУАЛЬНЫЕ КОДЫ КЛАВИШ Идентификатор Значение (в 16 с/с) Описание vk_LButton $01 Левая кнопка мыши vk_RButton $02 Правая кнопка мыши vk_Cancel $03 Используется для прерывания некоторого 14 Зак. 1049
402 Программирование в среде Borland Pascal для Windows Продолжение прил. В Идентификатор Значение Описание (в 16 с/с) процесса (Ctrl+Break) vk_MButton $04 Средняя кнопка мыши (3-кнопочная мышь) S05-S07 Не используется vk.Back $08 Клавиша Backspace vk_Tab $09 Клавиша Tab $0A,$0B Не используется vk_Clear $0C Клавиша Clear vk_Retum SOD Клавиша Enter $0E,$0F Не используется vk.Shift $10 Клавиша Shift vk_Control $11 Клавиша Ctrl vk_Menu $12 Клавиша Alt vk_Pause $13 Клавиша Pause vk_Capital $14 Клавиша Caps Lock. ... $15-$19 Зарезервировано для системы Kanji $1A Не используется vk_Escape SIB Клавиша Esc $1C,$1F Зарезервировано для системы Kanji vk_Space $20 Клавиша Spacebar (“Пробел”) vk_Prior $21 Клавиша “Страница вверх” vk_Next $22 Клавиша “Страница вниз” vk_End $23 Клавиша End vk.Home $24 Клавиша Ноте vk_Left $25 Клавиша “Стрелка влево” vk_Up $26 Клавиша “Стрелка вверх” vk.Right $27 Клавиша “Стрелка вправо” vk_Down $28 Клавиша “Стрелка вниз” vk_Select $29 Клавиша Select (“Выбор”) $2A OEM-зависимая клавиша vk.Execute $2B Клавиша Execute vk_SnapShot $2C Клавиша Print Screen в Windows vkjnsert $2D Клавиша Ins vk_Delete S2E Клавиша Del vk.Help $2F Клавиша Help
Приложения 403 Продолжение прил. В Идентификатор Значение Описание (в 16 с/с) vk_0 $30 Клавиша 0 vk_l $31 Клавиша 1 vk_2 $32 Клавиша 2 vk_3 $33 Клавиша 3 vk_4 $34 Клавиша 4 vk_5 $35 Клавиша 5 vk_6 $36 Клавиша 6 vk_7 $37 Клавиша 7 vk_8 $38 Клавиша 8 vk_9 $39 Клавиша 9 $3A-$40 Не используется vk_A $41 Клавиша А vk_B $42 Клавиша В vk_C $43 Клавиша С vk_D $44 Клавиша D vk_E $45 Клавиша Е vk_F $46 Клавиша F vk_G $47 Клавиша G vk_H $48 Клавиша Н vk_I $49 Клавиша I vk_J $4A Клавиша J vk_K $4B Клавиша К vk_L $4C Клавиша L vk_M $4D Клавиша М vk_N $4E Клавиша N vk_O $4F Клавиша О vk_P $50 Клавиша Р vk_Q $51 Клавиша Q vk_R $52 Клавиша R vk_S $53 Клавиша S vk_T $54 Клавиша Т vk_U $55 Клавиша U vkV $56 Клавиша V vk_W $57 Клавиша W
404 Программирование в среде Borland Pascal для Windows Продолжение прил. В Идентификатор Значение (в 16 с/с) Описание vk_X $58 Клавиша X vk_Y $59 Клавиша Y vk_Z $5A Клавиша Z $5B-$5F Не используется vk_NumPadO $60 Клавиша 0 на дополнительной клавиатуре vk_NumPadl $61 Клавиша 1 на дополнительной клавиатуре vk_NumPad2 $62 Клавиша 2 на дополнительной клавиатуре vk_NumPad3 $63 Клавиша 3 на дополнительной клавиатуре vk_NumPad4 $64 Клавиша 4 на дополнительной клавиатуре vk_NumPad5 $65 Клавиша 5 на дополнительной клавиатуре vk_NumPad6 $66 Клавиша 6 на дополнительной клавиатуре vk_NumPad7 $67 Клавиша 7 на дополнительной клавиатуре vk_NumPad8 $68 Клавиша 8 на дополнительной клавиатуре vk_NumPad9 $69 Клавиша 9 на дополнительной клавиатуре vk_Multiply $6A Клавиша умножения vk_Add $6B Клавиша сложения vk_Separator $6C Клавиша-разделитель vk_Subtract $6D Клавиша вычитания vk_Decimal $6E Клавиша десятичного разделителя vk_Divide $6F Клавиша деления vk_Fl $70 Клавиша F1 vk_F2 $71 Клавиша F2 vk_F3 $72 Клавиша F3 vk_F4 $73 Клавиша F4 vk_F5 $74 Клавиша F5 vk_F6 $75 Клавиша F6 vk_F7 $76 Клавиша F7 vk_F8 $77 Клавиша F8 vk_F9 $78 Клавиша F9 vk_F10 $79 Клавиша F10 vk.Fll $7A Клавиша Fl 1 vk_F12 $7B Клавиша F12 vk_F13 $7C Клавиша F13 vk_F14 $7D Клавиша F14
Приложения 405 Окончание прил. В Идентификатор Значение (в16с/с) Описание vk_F15 $7E Клавиша Fl5 vk_F16 $7F Клавиша F16 vk_F17 $80 Клавиша Fl7 vk_F18 $81 Клавиша Fl8 vk_F19 $82 Клавиша F19 vk_F20 $83 Клавиша F20 vk_F21 $84 Клавиша F21 vk_F22 $85 Клавиша F22 vk_F23 $86 Клавиша F23 vk_F24 $87 Клавиша F24 $88-$8F Не используется vk_NumLock $90 Клавиша Num Lock vk_Scroll $91 Клавиша Scroll Lock $92-$B9 Не используется $BA-$C0 OEM-зависимая клавиша $C1-$DA Не используется $DB-$E4 OEM-зависимая клавиша $E5 Не используется $E6 OEM-зависимая клавиша $E7,$E8 Не используется $E9-$F5 OEM-зависимая клавиша $F6-$FE Не используется Г. ДИРЕКТИВЫ КОМПИЛЯТОРА В следующих двух таблицах перечисляются директивы компилятора Borland Pascal. Директивы компилятора задаются двумя способами: • устанавливаются в интегрированной среде BPW; • помещаются непосредственно в исходный текст программы. Для установки директив компилятора в интегрированной среде служит диалоговое окно Compiler Options, которое открывается при выборе команды меню Options-Compiler.... Директива, помещаемая в текст программы, заключается в фигур- ные скобки или двухсимвольные ограничители (* и *) (подобно ком- ментариям) и состоит из символа $, за которым следует буква англий- ского алфавита, обозначающая директиву. После буквы пишется сим-
406 Программирование в среде Borland Pascal для Windows вол + или - (для директив-переключателей - табл. Г.1) или параметр, отделенный пробелами (для параметризованных директив - табл. Г.2). Например, чтобы включить директиву расширенного синтаксиса язы- ка Borland Pascal, в тексте программы необходимо написать: {$Х+} Директивы можно группировать, разделяя их запятыми, например: {$I-,N-,R+} Если в окне редактора Borland Pascal нажать два раза подряд кла- виши Ctrl+O, то в начало исходного текста будет помещен список всех директив-переключателей с их текущими значениями. Директи- вы, помещаемые в текст программы, имеют приоритет над теми, что установлены в IDE. Табл. ГЛ. Директивы-переключатели Директива и ее название в диалоговом окне Compiler Options G или L* Описание SA+/- Word align data G Если директива включена, то данные выравни- ваются по четным адресам. В этом случае дос- туп к ним осуществляется быстрее при работе программы на процессорах 80x86. Для процес- сора 8088 выравнивание данных не имеет эф- фекта SB+/- Complete boolean evaluation L Управляет вычислением булевских выражений. Если директива выключена, то вычисление бу- левских выражений прерывается, как только результат становится предопределен. Например, в выражении if False and MyFunc ... функция MyFunc никогда не будет вызвана. Если же директива включена, то в булевских выражениях всегда вычисляются все термы (рекомендуется использовать лишь в особых случаях) SD+/- Debug information G Если директива включена, то компилятор по- мещает в исполняемый файл отладочную ин- формацию. При этом должна быть включена опция Debug info in ЕХЕ в диалоговом окне установки параметров редактора связей, кото- рое вызывается по команде меню Options-Linker; иначе состояние директивы SD игнорируется SE+/- G Если директива включена, компилятор встраи-
Приложения Продолжение табл. Г.1 Директива и ее название в диалоговом окне Compiler Options G или L Описание Emulation вает в программу код обнаружения сопроцессо- ра 80x87. Если он присутствует в системе, то используется в операциях с вещественными числами. В противном случае работа сопроцес- сора 80x87 эмулируется. Если директива вы- ключена, то код эмуляции не встраивается в программу. Директива $Е недоступна при ком- пиляции программы для Windows $F +/- Force far calls L Включенное состояние директивы указывает на использование дальней (far) модели вызова для процедур и функций. Если директива выключе- на, то компилятор автоматически определяет, какую модель вызова - дальнюю (far) или ближ- нюю (near) - следует использовать. В этом слу- чае для процедур и функций, объявленных в интерфейсной части модуля, используется far- модель, а для всех остальных - пеаг-модель SG+/- 286 instructions G Если директива выключена, компилятор гене- рирует только инструкции процессора 8086, а значит, программу можно исполнять на всех процессорах семейства 80x86. При включенном состоянии этой директивы компилятор может генерировать дополнительные инструкции про- цессора 80286 (используется тогда, когда про- грамма предназначена для запуска на процессо- рах 80286, 80386, 80486 и выше) $1+1- 1/0 checking L Разрешает или запрещает контроль ошибок после каждой операции ввода-вывода. Если контроль выключен, то, чтобы узнать, была ли ошибка ввода-вывода, необходимо проверить значение переменной lOResult: (lOResult о 0) SK+/- Smart callbacks G С помощью данной директивы можно устранить необходимость обращений к Make Proc Inst а псе и FreeProcInstance для callback-функций SL+Z- Local symbols G Управляет генерацией символьной информации для локальных процедур и функций, перемен- ных и констант. Использование символьной информации значительно облегчает отладку программы с помощью Turbo Debugger. Если опция Debug info in ЕХЕ в окне Linker Options выключена или директива $D находится в со-
408 Программирование в среде Borland Pascal для Windows Продолжение табл. Г. 1 Директива и ее название в диалоговом окне Compiler Options G или L Описание $N+/- G стоянии {$D-}, то директива $L игнорируется Когда директива включена, компилятор напря- 8087/80287 SO +/- G мую генерирует инструкции сопроцессора в операциях с вещественными числами и позволя- ет использовать четыре дополнительных типа даннах: Single, Double, Extended и Comp. Если данная директива выключена, то вычисления с вещественными числами выполняются через обращение к соответствующим функциям биб- лиотеки времени исполнения. Если директива установлена в состояние {SN+}, то независимо от того, есть сопроцессор или нет, должна при- сутствовать библиотека WIN87EM.DLL Разрешает или запрещает генерацию оверлейно- Overlays allowed $P +/- G го кода при компиляции модуля для MS-DOS. Вместе с директивой {SO+} следует всегда ис- пользовать директиву {SF+}. Директива $О не применима при компиляции Windows-программ, в которых замещение кодовых сегментов под- держивается автоматически на уровне операци- онной системы Разрешает или запрещает передачу в парамет- Open parameters SQ+Z- L рах процедур и функций строк различной длины (тип String) вместе с информацией об их макси- мальных размерах Устанавливает или отменяет контроль перепол- Overflow checking SR +/- L нения целых чисел. Установку {SQ+} следует использовать только для отладки программы Устанавливает или отменяет контроль для ин- Range checking $S +/- L дексов массивов и переменных с типом диапазо- на. Эта директива позволяет также обнаружи- вать ошибочные ситуации, обусловленные вы- зовом виртуального метода объекта до вызова его конструктора. Состояние директивы {SR+} следует использовать только для отладки про- граммы Включает или выключает контроль переполне- Stack checking ния стека. В состоянии {SS+} компилятор встав- ляет в начало процедур и функций код провер- ки, который определяет, достаточно ли места на
Приложения 409 Окончание табл. Г. 1 Директива и ее название в диалоговом окне Compiler Option* G или L Описание $Т+/- G стеке для их выполнения. Обычно директиву SS лучше оставлять в состоянии {SS+}, так как это помогает предотвратить крах из-за нехватки стековой памяти. Для критичных по времени процедур и функций ее следует устанавливать в выключенное состояние Определяет, как осуществляется контроль со- Typed @ operator $V+/- L вместимости типов для указателей, полученных с помощью применения оператора @. При включенном состоянии директивы результат применения оператора @ к переменной типа Т есть указатель с типом ЛТ. В состоянии {$Т-} результатом всегда является нетипированный указатель (тип Pointer) Определяет, как осуществляется контроль со- Strict var-strings $W +/- L вместимости типов для строковых параметров при вызове процедур и функций. При включен- ном состоянии директивы требуется строгое совпадение по типу формального и фактическо- го параметров Управляет генерацией входного и выходного Windows stack frame $X +/- L кода для процедур и функций. Данная директи- ва должна быть установлена в выключенное состояние, так как современные версии Windows (3.1 и более поздние) работают только в защи- щенном режиме процессора Когда опция включена, функции можно вызы- Extended syntax SY +/- L вать так же, как и процедуры. В этом случае возращаемый результат игнорируется. Рекомен- дуемое состояние директивы - {$Х+} Позволяет генерировать символьную ссылоч- Symbol information ную информацию для модулей и программ, которая используется подсистемой^ Object Browser, встроенной в IDE Borland Pascal ♦ G - директива глобальная; L - директива локальная. Табл. Г.2. Директивы с параметрами Директива Описание SC Attribute Определяет атрибуты кодового сегмента: MOVEABLE или
410 Программирование в среде Borland Pascal для Windows Продолжение табл. Г.2 Директива Описание Attribute Attribute FIXED, DEMANDLOAD или PRELOAD, DISCARDABLE или PERMANENT. Первая пара атрибутов (MOVEABLE или FIXED) определяет, может ли сегмент перемещаться по памяти. Windows может перемещать сегменты с атрибутом MOVEABLE. Сегменты с типом FIXED никогда не изменя- ют своей адресной привязки. Атрибуты второй пары (DEMANDLOAD или PRELOAD) управляют загрузкой модуля в оперативную память. Модуль, имеющий атрибут DEMANDLOAD, загружается в память только в случае необходимости, а все остальное время остается на диске. Атрибут PRELOAD указывает на необходимость загружать модуль вместе с запуском программы. Наконец, последняя пара атрибутов (DISCARDABLE или PERMANENT) управляет выгрузкой модуля из памяти. Модуль с атрибутом DISCARDABLE может при необходимости выгружаться системой Windows из оперативной памяти. Модуль, имею- щий атрибут PERMANENT, остается в памяти на протяже- нии всей работы программы. Если опция $С опущена, то по умолчанию принимаются следующие установки, обеспечи- вающие наиболее эффективное использование памяти: {SC MOVEABLE DEMANDLOAD DISCARDABLE} $D Text Вместо слова Text могут быть помещены любые замечания по поводу названия и назначения программного продукта, а также авторских прав. Этот текст помещается в заголовок ЕХЕ-файла исполняемой программы или DLL-файла дина- мической библиотеки. Например, в текст программы можно вставить директиву {$D The Best Program. Copyright (с) 1995 by Me}. Данная директива не может использоваться внутри модулей $G UnitName, UnitName, UnitName Сообщает редактору связей о необходимости группировать указанные модули в один кодовый сегмент. Директива SG используется для более оптимальной группировки модулей, содержащих выгружаемый код. Она обеспечивает одновре- менный своппинг модулей, перечисленных в параметрах директивы. Модули должны иметь идентичные атрибуты (их можно задать с помощью директивы $С) и в сумме не долж- ны превышать размер сегментов, установленный с помощью директивы SS. В противном случае директива SG не произ- водит никакого эффекта $1 FileName Включает содержимое указанного файла в то место, в кото- ром употреблена директива. Эффект от использования дан- ной директивы аналогичен тому, как если бы содержимое включаемого файла было составной частью исходного тек- ста. Во включаемых файлах можно также использовать ди-
Приложения 411 Окончание табл. Г. 2 Директива Описание рективу $1, однако глубина вложенности таких включений не должна быть больше 15 уровней $L FileName Подключает указанный объектный модуль (OBJ-файл) или библиотеку (LIB-файл) к компилируемому модулю. Обычно используется для подключения модулей, написанных на ассемблере, С или другом языке программирования $М StackSize, Позволяет задать размер стека и объем локальной heap- HeapSize памяти, необходимых программе или динамической библио- теке. Параметр StackSize задает число байт, резервируемых под стек, и должен находиться в диапазоне от 4096 до 65520. Так как динамическая библиотека всегда использует стек вызвавшего ее приложения, то параметр StackSize для нее игнорируется. HeapSize указывает объем локальной heap- памяти программы и может принимать значения от 0 до 65520. (Глобальная память в отличие от локальной может быть сколь угодно большой; она доступна через обращения к New и GetMem.) Сумма StackSize и HeapSize, а также разме- ра в байтах всех глобальных переменных не должна превы- шать 65520 $R FileName Позволяет подключить к результирующей ЕХЕ-программе или DLL-библиотеке двоичный файл ресурса (который обычно имеет расширение .RES). Если файл ресурса под- ключается внутри модуля, то компилятор просто запоминает его имя в создаваемом TPW-файле. В этот момент файл ре- сурса может и не существовать. Все файлы ресурсов действи- тельно подключаются к программе на этапе ее финальной сборки $S SegSize Сообщает редактору связей предпочтительный размер кодо- вых сегментов. Редактор связей группирует небольшие по размеру модули так, чтобы в сумме их кодовые сегменты не превышали SegSize. Модули, большие по размеру, чем SegSize, помещаются в отдельные кодовые сегменты. Если в качестве параметра директивы указан 0, то все модули про- граммы или динамической библиотеки располагаются в своих собственных кодовых сегментах. Размер, указанный с помощью данной директивы, также относится и к модулям, группируемым с помощью директивы SG
ИНДЕКС А Автопрокрутка 148 Администратор MDI 295 Акселератор 181 Алгоритм сжатия данных RLE 207 Атрибуты окна 56 Б Бета-тестирование 385 Библиотека OWL 43 Библиотека импорта 315, 324 --STRLIB 326 Битовый образ (bitmap) 206 Блок 14 - группы (group box) 269 -диалога 227 - - общеупотребительный . (common dialog) 245 - памяти выгружаемый 340 - - неперемещаемый 340 --перемещаемый 340 - списка (list box) 10, 12, 277 Буфер обмена (Clipboard) 16, 336 - передачи данных 284 В Валидатор 275 Виртуальный индекс 62, 63 Вставка (Insert) 7, 14 д Данные управляющих объектов 286 Двухкратный щелчок 64 Дескриптор (handle) 26 - HInstance 36 -HPrevInst 36,46 - HWindow 52 -кисти 36 -курсора 36 -пиктограммы 35 -файла 26 - экземпляра приложения 35 Диалог выбора цвета 245 --шрифта 248 Диапазоны сообщений 63, 64 Динамическая библиотека DDEML 351 --ENTREXIT 328 — MATH 317 --STRDLL 326 Динамически подключаемая библиотека (DLL) 314 Динамический обмен данными (DDE) 349 Динамическое связывание 315 Диспетчер 62 Диспетчирование 29,62 - сообщений 34,40 Документ 293 Драйвер 30, 77 3 Запись Attr 56 Захват мыши (mouse capture) 121 И Идентификатор асинхронной транзакции 357 -меню 36 - оконного класса 36 -ресурса 155 -таймера 73 - управляющего элемента 263 Иерархия диалоговых и управляющих объектов 234 - объектов OWL 44 - основных оконных объектов 52 Импорт динамический 318, 320 - статический 318,319 --поимени 319 --поиндексу 319 — по новому имени 319 -функций 318 Имя оконного класса 36 - службы (service name) 350 - темы (topic name) 350, 351 - функции в DLL-библиотеке 316
Индекс 413 - элемента данных (item name) 350,351 Индекс диспетчирования 62 - функции в DLL-библиотеке 316 Индикатор предыдущего состояния 70 - расширенной клавиши 70 - системной клавиши 70 - текущего состояния 70 Инициализация приложения 46 Инсталляция 5,4 Интегрированная среда разработчика (IDE) 5 Интервал времени 72 Интерфейс MDI 293 - графических устройств (GDI) 77 - прикладной программы (API) 24 Источник событий 30 К Кисть 89,103 Клавиша акселераторная 8, 181 - “горячая” 7, 181 -символьная 183 Клиент 350, 373 Кнопка (button) 10 -автоматическая 268 - зависимой фиксации (radio button) 10,268 -максимизации 9 -минимизации 9 - нажимающаяся (pushbutton) 265 - независимой фиксации (checkbox) 10,266 - селективная (radio button) 268 Код OEM 70 - завершения динамической библиотеки 327 - инициализации динамической библиотеки 327 - клавиши виртуальный 69, 184 - растровой операции 210, 211, 215 -символа 69 Кодовая таблица ANSI 20 --OEM 20 --символов 19 Команда меню 6 --системного 9 - вставки и удаления 15 - манипуляции блоками текста 15 - перемещения курсора 14 - работы с блоками 15 Комбинированный блок (combo box) 11, 12,280 Конструктор InitResource 236 Контекст дисплея 79 -отображения 78 -устройства 78 --виртуальный 209 --метафайловый 124 Координаты окна 37 Коэффициент масштабный 109 --обратный 109 - пропорциональности пиксела (aspect ratio) 114 Курсор (cursor) 197 М Маска AND 187, 188 -XOR 187, 188 Меню 26 -вертикальное 159 -всплывающее 159 -главное 6 -горизонтальное 6, 159 -динамическое 175 -колончатое 159 -локальное 179 -системное 9, 175 Метафайл 123 Метод BeginDocument 308 - BeginPrinting 308 -CanClose 75 -CloseWindow 238 -Configure 313 -Create 53 -CreateChild 296 - DefWndProc <259 - EnableTransfer 285 -EndDlg 238 -EndDocument 308 -EndPrinting 308 - Error 48, 226 -ExecDialog 237 - Execute 235
414 Программирование в среде Borland Pascal для Windows -GetClassName 67,68 - GetSelection 308 -GetWindowClass 67 -HasNextPage 308 -IdleAction 174,301 - InitAbortDialog 313 - InitApplication 46, 47 -InitChild 296 - Initlnstance 46, 47, 54 -InitMainWindow 45 - InitPrintDialog 313 - InitSetupDialog 313 - IsVisibleRect 149 -MakeWindow 53 -Paint 81 - Print 306, 307 -PrintPage 307 -ReportError 313 -Run 46 -ScrollBy 152 -ScrollTo 152 -SetCaption 68 -SetDevice 312 -SetRange 149 -SetUnits 148, 152 - SetValidator 275 -Setup 305 -SetupWindow 54 -Show 53 - TransferData 285 -динамический 62 - ответа на команду 164 ----сообщение 62,63 Механизм “перетяни и оставь” (drag-and-drop) 331 Многооконный интерфейс (MDI) 293 Множество команд (command set) 8 - - CUA (Common User Access) 8 - -среды BPW 8 Модель вызова 316 Модуль BITMAPS 219 -BWCC 231,263 -COMMDLG 245 -ODIALOGS 261 -OPRINTER 303 -OSTDDLGS 240 -OSTDWNDS 298 -OWINDOWS 45 -SHELLAPI 331 -STRUNIT 325 -SYSTEM 18 -TOOLBAR 256 -WIN31 24,60 -WINCRT 18 -WINPROCS 24 -WINTYPES 24,26 - импорта 324 H Нарушение общей защиты памяти (GPF) 387 Номер сообщения 63 Нотация 24 О Область обновления (update region) 80 - окна рабочая (client area) 7, 80 - экрана рабочая (desktop) 27 Обработка ошибок 48 -сообщений 40 - -в OWL 61 Объект TApplication 45 -TButton 265 -TCheckBox 266 -TComboBox 280 -TControl 52,234,236 -TDialog 52, 234 -TDlgWindow 234 -TEdit 272 - TEditPrintout 311 -TFileDialog 240 -TFileWindow 298 - TFilterValidator 275 -TGroupBox 269 -TInputDialog 240 -TListBox 277 -TMDIWindow 52,295 - TPXPictureValidator 275 - TPrintDialog 313 -TPrintOut 306 -TPrinter 304 - TPrinterAbortDlg 313 - TPrinterSetupDlg 313 - TRadioButton 268
Индекс 415 - TRangeValidator 275 -TScrollBar 282 -TScroller 149 -TStatic 270 - TStringLcfokupValidator 275 -TValidator 275 -TWindow 45,52 -TWindowPrintout 311 - TWindowsObject 52 - диалога стандартный 240 -интерфейсный 51,235 -оконный 51 -прикладной 45 Объектная технология 26 Объекты Windows 26 Окно 8, 26 - Application Error 48, 49 -активное 9 - выбора принтера 305 - главное 6, 26 - диалога (dialog box) 10, 227 --модальное 228 --немодальное 228 --системно-модальное 228 -дочернее 27 --MDI 293,296 -логическое НО - обрамляющее (frame window) 293, 294 - проекта в Resource Workshop 156 -проекции ПО - рамки (frame window) 293 -редактора 9 -родительское 27 Окно-клиент 294, 295 Оконный класс 34 --стандартный 34 - - управляющего элемента 229 Операционная система MS-DOS 5, 18 Операция растровая 144 Отладка программы 384 , Отладчик Turbo Debugger 21, 387 -автономный 387 Отношение “родитель-ребенок” 27, 52 Отсечение (clipping) 81 Очередь сообщений системная 30 - - прикладной программы 30 Ошибка времени выполнения (run-time error) 20, 383 -кодирования 386 -синтаксические 383 - смысловая (логическая) 383, 384 -спецификации 386 Ошибки зарезервированные 47,48 П Память локальная динамическая 223 - расширенная (extended memory) 5 Панель данных 394, 395 - инструментальная (toolbar) 6, 256 --вертикальная 258 --горизонтальная 258 - машинного кода 394 - регистров 394, 395 - селекторов 394, 395 - состояния (status bar) 7, 14 - стека 394, 395 - флагов процессора 394, 395 Передача данных 284 Перезапись (overwrite) 7, 14 Переменная CmdShow 54 -ExitCode 328 Перенос программ из MS-DOS в Windows 18, 19 Перерисовка окна 80 Переходник (thunk) 315 Перо 89, 101 Пиктограмма (icon) 6, 187 Пиктограмма динамичная 195 Поле выбора 10,11 -действия 10,11 -проверки 10,11 Полоса скроллинга (scroll bar) 9/282 Посылка сообщений 41 Префиксы имен 24, 25 Привязка и внедрение объектов (OLE) 382 Прием сообщений 39 Приложение активное 31 Прилржение-монитор DDE 363
416 Программирование в среде Borland Pascal для Windows Принцип WYSIWYG 78 - “клиент-сервер” 349, 350 Принципы GDI 77 - построения MDI-приложений 293 Программа ABOUTBOX 239 -ACCEL 186 -BRUSHES 105 -CHART 130, 131 -CHECKS 167, 168 -CLIPBMP 344 -CLOSEWND 76 -COMMDIAL 253 -CONTROLS 289 -CURSOR 199 -DDECLNT 373 -DDESRVR 364 - DRAGDROP 333 -DRAWBMP 216 - DRAWTXT 95 -DYNICON 195 -EEDEMO 329 -ERRMSG 48,49 -FIRST 18 - FONTDEMO 99 -GRAYMENU 171, 172 -ICON 192 -MAPMODES 114, 115 - MDIEDIT 299 -METAFILE 126 - MINMDI 296, 297 -MINOWL 43_46 - MINWIN 32, 33 -MOUSEDRAW 118 - MOUSEMSG 65 - PAINT 82 -PENS 102 -POPUP 180 - PRNTEST 308, 309 - RANDRECT 85 -SCROLL 149 -SETCURS 205 - SETUPWND 54, 55 -SHOWBMP 212 -SHOWCURS 201 -SHOWICON 193 - SINGLE 47 -SLIDES 219 -STDDIAL 243 -STRTABLE 226 -SYSMENU 177 -TESTDLL 327 -TIMERMSG 72 -TIMERMSG 73 -TOOLS 259 -TXTOUT 93 -USEMATH 319 -USEMATH2 322 -WNDATTR 60,61 - перекодировки файлов FCONVERT 20 - с событийно-управляемой архитектурой 28 Программа-русификатор 19 Проект в Resource Workshop 156 Прокрутка см. Скроллинг Пролог (prolog) 316 Просмотр (browsing) объектов 21 Протокол динамического обмена данными (DDE) 349 Протокол привязки и внедрения объектов (OLE) 382 Процедура DragFinish 333 -EndPaint 81 - GetClientRect 83 - GetWindowRect 55 -MoveWindow 56 -ReleaseCapture 121 Пункт меню 159 — запрещенный (disabled) 159, 166, 171 - - непомеченный (unchecked) 159 --нетерминальный 159 --помеченный (checked) 159,165 - - разрешенный (enabled) 159, 166, 171 — терминальный 159 - - тусклый (grayed) 159 Р Распечатка 306 Растровое изображение (bitmap) 206 - - аппаратно-зависимое 206 - - аппаратно-независимое (DIB) 206 Регистрация оконного класса 33, 36
Индекс 417 Редактор графический 130 -диалогов 231 -меню 160 -пиктограмм 189 - растровых изображений 207 -ресурсов 154 - -Resource Workshop 21 - таблицы акселераторов 183 - таблицы строк 224, 225 - текста (edit box) 272 --многострочный 272 --однострочный 272 -текстовый 12 --многооконный 298 -шрифтов 97 Режим вставки 7, 14 - коррекции изображения 211 - масштабирования 112 --принудительный 114 - - частично принудительный 114 - набора текста 7 -перезаписи 7, 14 - процессора защищенный (protected mode) 5 - - реальный (real mode) 5 - фонаOpaque 88 --Transparent 88 Ресурс пользовательский 255 - разделяемый (shared) 379 -Windows 153 С Связь “горячая” (hot advise loop) 359 - “теплая” (warm advise loop) 359 Селектор 395 -диапазона 63 Семейство шрифта 98 Сервер 350, 364 Система координат 108 --логическая 108 --физическая 108 Системные метрики Windows 55, 400 Скан-код 70 Скроллинг 148 Событие 28 - в системе Windows 29 Соединение (conversation) 350, 356 Сообщение 29 - wm_ActivateApp 72 -wm_Char 69 -wm_Close 40 -wm_DeadChar 69 -wm_Destroy 40 - wm.DropFiles 332, 333 -wm_KeyDown 69 -wm_KeyUp 69 - wmJLButtonDblClk 65 - wm_LButtonDown 64 - wm.LButtonUp 64 - wm_MButtonDblClk 65 - wm_MButtonDown 65 - wm_MButtonUp 65 - wm_MDIGetActive - wm_MouseMove 65 -wm_NCPaint 80 - wmJPaint 38, 80 -wm_Quit 40 - wm_RButtonDblClk 65 - wm^RButtonDown 65 - wm_RButtonUp 65 -wm_Size 259 -wm_SysChar 69 - wm_SysCommand 176 - wm.SysDeadChar 69 - wm_SysKeyDown 69 -wm_SysKeyUp 69 - wm_Timer 72 -клавиатуры 68 -мыши 64 -обошибке 20 -символьное 69 -таймера 72 -уведомительное 264 -управляющее 264 - управляющего элемента 264 Составной документ (compound document) 382 Список дочерних объектов 52 Справочная информация 23 Спрайт 188 Статический текст (static text) 270 Стиль блока списка 278 -кисти 105 -кнопки 267
418 Программирование в среде Borland Pascal для Windows - комбинированного блока 281 - окна 37,56 - 59 - - дополнительный 56, 59, 60 - оконного класса 34, 35 -пера 101 - полосы скроллинга 283 - текстового редактора 274 -штриховки 104 Схема обработки сообщений 30, 31 Счетчик повтора 70 - привязок (use counter) 315 Т Таблица акселераторов 182 - динамических методов (DMT) 62 - нерезидентных имен 317 - резидентных имен 317 -строк 223 Таймер 72 Твердая копия 303 Тестирование 384 Тип данных PChar 25 --TChooseColor 245 --TChooseFont 249 --TColorRef 87 - - TComboBoxRec 287 - - TListBoxMultiRec 286 - - TListBoxSingleRec 286 --TLogBrush 104 --TLogFont 97 --TLogPen 102 --TMessage 63 --TMsg 29 — TMultiSelRec 287 --TRect 55 - - TScrollBarTransferRec 287 --TWindowAttr 56 --TWndClass 34 - дескриптора HAccTable 184 --HBitmap 208 --HBrush 89 --HCursor 198 --HFont 89 --Hlcon 191 --HMenu 164 --HPen 89 --HWnd 36 --THandle 26 Точка “горячая” (hot spot) 197 - останова 385,390 --глобальная 390 --условная 391 Транзакция (transaction) 350,357,360 - 363 -асинхронная 357 -синхронная 357 Трансляция сообщений 34, 39 У Указатель Application 46 - ChildList 52 -ClientWnd 295 -MainWindow 45 -Parent 52 - TransferBuffer 235, 285 Управление событийное 28 Управляющие объекты OWL 261 - элементы (controls) 261 --базовые 229 --дополнительные 229 Утилита Turbo Profiler 21 Утилита WinSight 21 Ф Файл AUTOEXEC.BAT 6 -ВР.ЕХЕ 5 - BPW.EXE 5 -CONFIG.SYS 6 -TURBO.EXE 5 - выполнимый (executable) 20 - объявления констант 1-55 - описания ресурсов 154 -ресурса 154 Фильтрация клавиатурного ввода 276 -сообщений 396 Фокус ввода 30 Формат данных 338 Функция AllocMultiSel 287 -AppendMenu 176 -BeginPaint 81 - BitBlt 210 - callback 34, 72, 360 - CheckMenuItem 165 -ChooseColor 245 - ChooseFont 245, 248
Индекс 419 - CloseClipboard 337 -CloseMetaFile 124 - CountClipboardFormats 343 - CreateBrushlndirect 104 - CreateCompatibleBitmap 214 - CreateCompatibleDC 209 - CreateDialogParam 237 - CreateFont 97 - CreateFontlndirect 97 - CreateHatchBrush 104 -CreateMetaFile 123 - CreatePattemBrush 104, 218 -CreatePen 101 - CreatePenlndirect 101 - CreatePopupMenu 179 - CreateSolidBrush 103 - CreateWindow 36 -DPtoLP 110 - DdeAccessData 355 -DdeCallbackProc 360 - DdeClientTransaction 358 - DdeCmpStringHandles 354 -DdeConnect 356 - DdeCreateDataHandle 354 - DdeCreateStringHandle 354 - DdeFreeDataHandle 356 - DdeFreeStringHandle 354 -DdeGetData 355 - DdeGetLastError 353 -Ddelnitialize 351 -DdeNameService 356 - DdePostAdvise 359 - DdeUnaccessData 355 - DdeUninitialize 353 - DefWindowProc 41 - DeleteMetaFile 124 -DeleteObject 90 - DestroyCursor 201 - Destroy Menu 180 - Destroy Window 41 - DialogBoxParam 237 - DispatchMessage 40 -DragAcceptFiles 331 -DragQueryFile 332 -DragQueryPoint 336 -Drawicon 193 -DrawText 93 -DrawText 94 -Ellipse 83 -EmptyClipboard 338 - EnableMenuItem 171 - EnumClipboardFormats 343 -ExtTextOut 93 -Extracticon 336 - FindText 245 -FreeLibrary 321 - FreeMultiSel 287 - GetAsyncKeyState 71 -GetBValue 88 -GetBkColor 89 - GetClipboardData 341 - GetDC 85 -GetGValue 88 -GetKeyState 70 - GetMapMode 112 -GetMenuState 166 -GetMessage 39 -GetMetaFile 125 - GetNearestColor 88 - GetObject 91 - GetOpenFileName 245 -GetProcAddress 321 -GetROP2 144 -GetRValue 88 - GetSaveFileName 245 - GetStockObject 91 - GetStretchBltMode 212 - GetSystemMenu 175 - GetSystemMetrics 55 - GetTextMetrics 152 -GlobalAlloc 339 -GlobalLock 340 - GlobalUnlock 341 -GrayString 93 -InvalidateRect 84 - IsClipboardFormatAvailable 342 -Islconic 195 - IsWindowEnabled 73 - KillTimer 73 -LPtoDP 110 - LineTo 122 t> - LoadAccelerators 185 -LoadBitmap 208 - LoadBit map File 219 -LoadCursor 198 -Loadicon 191 -LoadLibrary 320 -LoadMenu 164
420 Программирование в среде Borland Pascal для Windows -LoadString 226 -MessageBox 50 -MoveTo 122 -OpenClipboard 337 - PatBlt 214 - PeekMessage 39 - PlayMetaFile 125 - PostMessage 42 - PostQuitMessage 41 - PrintDlg 245 - RGB 87 - Rectangle 87 - RegisterClass 36 - RegisterClipboardFormat 339 -ReleaseDC 85 -ReplaceText 245 -RestoreDC 143 -SaveDC 143 - SelectObject 89 -SendMessage 42 -SetCapture 121 -SetClassWord 200 - SetClipboardData 338 - SetCursor 204 - SetMapMode 112 -SetROP2 144 - SetStretchBltMode 211 -SetTimer 73 -SetViewportExt 112 -SetViewportOrg 111 -SetWindowExt 112 - SetWindowOrg 111 - StoreBitmapFile 219 - StretchBlt 210 - TabbedTextOut 93 -TextOut 93 -TrackPopupMenu 179 - TranslateMessage 39 - UpdateWindow 38, 84 -WVSPrintF 68 -WindowProc 40 - обработки сообщений 61 - оконная 31,40 -экспортируемая 40,316 ц Цвет 87 -Inverted 190 -Transparent 190 - в формате RGB 88 Цветовая составляющая 87 Цикл обработки событий 28 - - сообщений 31, 34, 38 - уведомления (advise loop) 358 -холостой 173 Ш Шаг шрифта 98 Шрифт 89, 96 - TrueType 78, 92, 96 -системный 97 Э Экземпляр программы 36 Элемент интерфейсный 51 -окна 27 - управления в окне (control) 261 Эпилог (epilog) 316
ОГЛАВЛЕНИЕ ПРЕДИСЛОВИЕ..........................................3 1. ИНТЕГРИРОВАННАЯ СРЕДА РАЗРАБОТЧИКА................5 1.1. ИНСТАЛЛЯЦИЯ И ЗАПУСК..........................5 1.2. ГЛАВНОЕ МЕНЮ..................................6 1.3. УПРАВЛЕНИЕ ОКНАМИ.............................8 1.4. ВСТРОЕННЫЙ РЕДАКТОР ТЕКСТОВ..................12 1.4.1. ОКНО РЕДАКТИРОВАНИЯ.....................12 1.4.2. КОМАНДЫ РЕДАКТОРА.......................13 1.5. ПЕРВАЯ ПРОГРАММА.............................17 2. ОСНОВЫ ПРОГРАММИРОВАНИЯ В WINDOWS................24 2.1. СОГЛАШЕНИЯ WINDOWS API.......................24 2.2. ОБЪЕКТЫ И ДЕСКРИПТОРЫ........................26 2.3. ОКНА.........................................26 2.4. СОБЫТИЙНОЕ УПРАВЛЕНИЕ........................28 2.5. СООБЩЕНИЯ....................................29 2.6. СХЕМА ОБРАБОТКИ СООБЩЕНИЙ....................30 2.7. WINDOWS-ПРОГРАММА............................32 2.7.1. “ПРОСТАЯ” ПРОГРАММА.....................32 2.7.2. РЕГИСТРАЦИЯ ОКОННОГО КЛАССА.............34 2.7.3. СОЗДАНИЕ ОКНА...........................36 2.7.4. ОТОБРАЖЕНИЕ ОКНА........................37 2.7.5. ОБНОВЛЕНИЕ РАБОЧЕЙ ОБЛАСТИ ОКНА.........38 2.7.6. ЦИКЛ ОБРАБОТКИ СООБЩЕНИЙ................38 2.7.7. ПРИЕМ СООБЩЕНИЙ.........................39 2.7.8. ТРАНСЛЯЦИЯ СООБЩЕНИЙ....................39 2.7.9. ДИСПЕТЧИРОВАНИЕ СООБЩЕНИЙ............. 40 2.7.10. ОКОННАЯ ФУНКЦИЯ........................40 2.7.11. ПОСЫЛКА СООБЩЕНИЙ ..;..................41 2.7.12. ПЕРВЫЕ ИТОГИ...........................42 3. ВВЕДЕНИЕ В OBJECT WINDOWS........................43 3.1. НАЗНАЧЕНИЕ БИБЛИОТЕКИ OBJECT WINDOWS.........43
422 Программирование в среде Borland Pascal для Windows 3.2. ПРОСТЕЙШЕЕ OWL-ПРИЛОЖЕНИЕ...................43 3.3. ИНИЦИАЛИЗАЦИЯ ПРИЛОЖЕНИЯ....................46 3.4. ОБРАБОТКА ОШИБОК............................48 3.5. ОКНА В OWL..................................51 3.6. СОЗДАНИЕ И ОТОБРАЖЕНИЕ ОКНА.................53 3.7. АТРИБУТЫ ОКНА...............................56 3.8. ОБРАБОТКА СООБЩЕНИЙ В OWL...................61 3.8.1. ОСНОВНАЯ ИДЕЯ...........................61 3.8.2. ДИНАМИЧЕСКИЕ ВИРТУАЛЬНЫЕ МЕТОДЫ.........62 3.8.3. МЕТОДЫ ОТВЕТА НА СООБЩЕНИЯ..............63 3.9. СООБЩЕНИЯ ОТ МЫШИ...........................64 ЗЛО. СООБЩЕНИЯ ОТ КЛАВИАТУРЫ.....................68 3.11. СООБЩЕНИЯ ОТ ТАЙМЕРА.......................72 3.12. УПОРЯДОЧЕННОЕ ЗАКРЫТИЕ ОКОН................75 4. ИНТЕРФЕЙС ГРАФИЧЕСКИХ УСТРОЙСТВ.................77 4.1. ВВЕДЕНИЕ В GDI............................. 77 4.1.1. GDI - ОСНОВА WINDOWS....................77 4.1.2. КОНТЕКСТ УСТРОЙСТВА.....................78 4.2. ВЫВОД ИНФОРМАЦИИ В ОКНО.....................79 4.2.1. ОКНО КАК ОБЪЕКТ ВЫВОДА..................79 4.2.2. ОТСЕЧЕНИЕ...............................80 4.2.3. РЕАКЦИЯ НА СООБЩЕНИЕ wm_Paint...........81 4.2.4. ПРИНУДИТЕЛЬНАЯ ПЕРЕРИСОВКА ОКНА.........84 4.3. УПРАВЛЕНИЕ ЦВЕТОМ...........................87 4.4. ГРАФИЧЕСКИЕ СРЕДСТВА РИСОВАНИЯ..............89 4.4.1. ПЕРЬЯ, КИСТИ, ШРИФТЫ....................89 4.4.2. СОЗДАНИЕ И УДАЛЕНИЕ СРЕДСТВ РИСОВАНИЯ...90 4.5. ВЫВОД ТЕКСТА................................92 4.6. УПРАВЛЕНИЕ ШРИФТАМИ.........................96 4.7. РИСОВАНИЕ ПЕРОМ............................101 4.8. РИСОВАНИЕ КИСТЬЮ...........................103 4.9. РЕЖИМЫ ОТОБРАЖЕНИЯ.........................108 4.10. РИСОВАНИЕ С ПОМОЩЬЮ МЫШИ..................117 4.11. МЕТАФАЙЛЫ.................................123 4.12. УЧЕБНЫЙ ГРАФИЧЕСКИЙ РЕДАКТОР..............130 4.13. СКРОЛЛИНГ ИЗОБРАЖЕНИЙ В ОКНЕ..............147
Оглавление 423 5. РЕСУРСЫ WINDOWS....................................153 5.1. ПОНЯТИЕ РЕСУРСОВ...............................153 5.2. СОЗДАНИЕ И ПОДКЛЮЧЕНИЕ РЕСУРСОВ................154 5.3. ПОДГОТОВКА НОВОГО ФАЙЛА РЕСУРСОВ...............156 5.4. МЕНЮ...........................................159 5.4.1. ОСНОВНЫЕ ПОНЯТИЯ..........................159 5.4.2. РАЗРАБОТКА МЕНЮ НА БАЗЕ RESOURCE WORKSHOP.159 5.4.3. РЕАКЦИЯ НА КОМАНДЫ МЕНЮ...................163 5.4.4. ПОМЕТКА ПУНКТОВ МЕНЮ......................165 5.4.5. РАЗРЕШЕНИЕ И ЗАПРЕЩЕНИЕ ПУНКТОВ МЕНЮ......171 5.4.6. ИЗМЕНЕНИЕ СИСТЕМНОГО МЕНЮ.................175 5.4.7. ЛОКАЛЬНОЕ МЕНЮ.......................... 178 5.5. АКСЕЛЕРАТОРЫ...................................181 5.5.1. ОБЩИЕ СВЕДЕНИЯ............................181 5.5.2. СОЗДАНИЕ ТАБЛИЦЫ АКСЕЛЕРАТОРОВ............182 5.5.3. ПОДКЛЮЧЕНИЕ АКСЕЛЕРАТОРОВ.................184 5.6. ПИКТОГРАММЫ....................................187 5.6.1. НАЗНАЧЕНИЕ ПИКТОГРАММ.....................187 5.6.2. РАЗРАБОТКА ПИКТОГРАММ.....................189 5.6.3. ПОДКЛЮЧЕНИЕ ПИКТОГРАММ....................190 5.6.4. ОТОБРАЖЕНИЕ ПИКТОГРАММ....................193 5.6.5. ДИНАМИЧНЫЕ ПИКТОГРАММЫ....................195 5.7. КУРСОРЫ........................................197 5.7.1. ЧТО ТАКОЕ КУРСОР..........................197 5.7.2. УСТАНОВКА КУРСОРА.........................198 5.7.3. ВРЕМЕННАЯ УСТАНОВКА КУРСОРА...............200 5.8. РАСТРОВЫЕ ИЗОБРАЖЕНИЯ..........................206 5.8.1. ОСНОВНЫЕ ПОНЯТИЯ..........................206 5.8.2. РАЗРАБОТКА РАСТРОВЫХ ИЗОБРАЖЕНИЙ..........207 5.8.3. ЗАГРУЗКА И ВЫВОД РАСТРОВЫХ ИЗОБРАЖЕНИЙ....208 5.8.4. РИСОВАНИЕ РАСТРОВЫХ ИЗОБРАЖЕНИЙ...........213 5.8.5. РАСТРОВОЕ ИЗОБРАЖЕНИЕ КАК ЗАПОЛНИТЕЛЫФОНА.217 5.8.6. РАБОТА С ВМР-ФАЙЛАМИ......................218 5.9. ТАБЛИЦА СТРОК..................................223 5.9.1. ОБЩИЕ СВЕДЕНИЯ............................223 5.9.2. СОЗДАНИЕ ТАБЛИЦЫ СТРОК.................. 224 5.9.3. ЗАГРУЗКА СТРОК............................225 5.10. ДИАЛОГОВЫЕ БЛОКИ..............................227
424 Программирование в среде Borland Pascal для Windows 5.10.1. ЧТО ТАКОЕ ДИАЛОГОВЫЙ БЛОК.................227 5.10.2. РАЗРАБОТКА ДИАЛОГОВЫХ БЛОКОВ..............229 5.10.3. ВЫПОЛНЕНИЕ ДИАЛОГОВЫХ БЛОКОВ..............233 5.10.4. ВЗАИМОДЕЙСТВИЕ С ЭЛЕМЕНТАМИ УПРАВЛЕНИЯ....238 5.10.5. ДИАЛОГОВОЕ ОКНО “ABOUT....................239 5.10.6. СТАНДАРТНЫЕ ДИАЛОГОВЫЕ ОБЪЕКТЫ OWL........240 5.10.7. СТАНДАРТНЫЕ ДИАЛОГОВЫЕ БЛОКИ WINDOWS......245 5.11. ИНСТРУМЕНТАЛЬНАЯ ПАНЕЛЬ....................... 255 5.11.1. ОБЩИЕ СВЕДЕНИЯ............................255 5.11.2. РАЗРАБОТКА РЕСУРСА ИНСТРУМЕНТАЛЬНОЙ ПАНЕЛИ.256 5.11.3. ПОДКЛЮЧЕНИЕ ИНСТРУМЕНТАЛЬНОЙ ПАНЕЛИ.......258 6. ЭЛЕМЕНТЫ УПРАВЛЕНИЯ В ОКНАХ.........................261 6.1. ОСНОВНЫЕ ЭЛЕМЕНТЫ УПРАВЛЕНИЯ....................261 6.2. СОЗДАНИЕ ЭЛЕМЕНТОВ УПРАВЛЕНИЯ...................263 6.3. ОБРАБОТКА СООБЩЕНИЙ ОТ ЭЛЕМЕНТОВ УПРАВЛЕНИЯ......264 6.4. ТИПЫ УПРАВЛЯЮЩИХ ОБЪЕКТОВ.......................265 6.4.1. ОБЪЕКТ TButton.............................265 6.4.2. ОБЪЕКТ TCheckBox...........................266 6.4.3. ОБЪЕКТ TRadioButton........................268 6.4.4. ОБЪЕКТ TGroupBox...........................269 6.4.5. ОБЪЕКТ TStatic.............................270 6.4.6. ОБЪЕКТ TEdit...............................272 6.4.7. ОБЪЕКТ TListBox............................277 6.4.8. ОБЪЕКТ TComboBox............................J 280 6.4.9. ОБЪЕКТ TScrollBar..........................282 6.5. РАБОТА С ДАННЫМИ................................284 6.5.1. МЕХАНИЗМ ПЕРЕДАЧИ ДАННЫХ...................284 6.5.2. ТИПЫ ДАННЫХ................................286 6.5.3. ПРИМЕР ВВОДА ДАННЫХ В ОКНЕ ДИАЛОГА.........287 7. ПРИЛОЖЕНИЯ С МНОГООКОННЫМ ИНТЕРФЕЙСОМ................293 7.1. ОСНОВЫ ОРГАНИЗАЦИИ MDI..........................293 7.2. КОМПОНЕНТЫ MDI-ПРИЛОЖЕНИЯ.......................294 7.2.1. ОБРАМЛЯЮЩЕЕ ОКНО...........................294 7.2.2. ОКНО-КЛИЕНТ................................295 7.2.3. ДОЧЕРНИЕ MDI-OKHA..........................296 7.3. ПРОСТЕЙШЕЕ MDI-ПРИЛОЖЕНИЕ.......................296
Оглавление 425 7.4. МНОГООКОННЫЙ ТЕКСТОВЫЙ РЕДАКТОР.................298 8. ВЫВОД НА ПРИНТЕР.................................. 303 8.1. ОСНОВНЫЕ ПОЛОЖЕНИЯ..............................303 8.2. ПЕЧАТЬ СРЕДСТВАМИ OWL...........................303 8.3. ДОПОЛНИТЕЛЬНЫЕ ВОЗМОЖНОСТИ МОДУЛЯ OPRINTER....311 9. ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ.................314 9.1. ОБЩИЕ ПОЛОЖЕНИЯ.................................314 9.2. НАПИСАНИЕ DLL-БИБЛИОТЕК.........................316 9.3. ИСПОЛЬЗОВАНИЕ DLL-БИБЛИОТЕК.....................318 9.3.1. СПОСОБЫ ИМПОРТА............................318 9.3.2. СТАТИЧЕСКИЙ ИМПОРТ.........................319 9.3.3. ДИНАМИЧЕСКИЙ ИМПОРТ........................320 9.4. МНОГОМОДУЛЬНЫЕ И ВЗАИМОЗАВИСИМЫЕ ДИНАМИЧЕСКИЕ БИБЛИОТЕКИ...........................................324 9.5. ИНИЦИАЛИЗАЦИЯ И ЗАВЕРШЕНИЕ РАБОТЫ DLL-БИБЛИОТЕК.. 327 9.6. ОГРАНИЧЕНИЯ ПРИ РАЗРАБОТКЕ DLL-БИБЛИОТЕК........329 10. ОБМЕН ДАННЫМИ МЕЖДУ ПРИЛОЖЕНИЯМИ...................331 10.1. ТЕХНОЛОГИЯ “ПЕРЕТЯНИ И ОСТАВЬ”.................331 10.1.1. ВИД СО СТОРОНЫ............................331 10.1.2. ПОДГОТОВКА К ПРИЕМУ ФАЙЛОВ................331 10.1.3. ПРИЕМ ФАЙЛОВ..............................332 10.1.4. ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ...................335 10.2. ПЕРЕДАЧА ДАННЫХ ЧЕРЕЗ БУФЕР ОБМЕНА.............336 10.2.1. НАЗНАЧЕНИЕ БУФЕРА ОБМЕНА..................336 10.2.2. ОТКРЫТИЕ И ЗАКРЫТИЕ БУФЕРА ОБМЕНА.........337 10.2.3. ПЕРЕДАЧА ДАННЫХ...........................338 10.2.4. ПРИЕМ ДАННЫХ..............................341 10.2.5. ПЕРЕДАЧА И ПРИЕМ РАСТРОВЫХ ИЗОБРАЖЕНИЙ....343 10.3. ДИНАМИЧЕСКИЙ ОБМЕН ДАННЫМИ МЕЖДУ ПРИЛОЖЕНИЯМИ (DDE)......................4............349 10.3.1. ОБЩИЕ ПОЛОЖЕНИЯ.........................349 10.3.2. ПОДКЛЮЧЕНИЕ К DDEML.......................351 10.3.3. УПРАВЛЕНИЕ ИМЕНАМИ........................353 10.3.4. УПРАВЛЕНИЕ БЛОКАМИ ДАННЫХ.................354 10.3.5. РЕГИСТРАЦИЯ СЛУЖБЫ И УСТАНОВКА СОЕДИНЕНИЯ.356 10.3.6. ТРАНЗАКЦИИ..............................357
assss sssзззаШ!за 426 Программирование в среде Borland Pascal для Windows 10.3.7. ПРИМЕР ДИНАМИЧЕСКОГО ОБМЕНА ДАННЫМИ..... 10.3.8. DDE В ЛОКАЛЬНОЙ СЕТИ.................... 10.3.9. ВЗАИМОДЕЙСТВИЕ С PROGRAM MANAGER........ 11. ОТЛАДКА ПРОГРАММ В WINDOWS....................... 11.1. ТИПЫ ОШИБОК.................................. 11.2. ПРОЦЕСС ОТЛАДКИ ПРОГРАММЫ.................... 11.2.1. ЭТАПЫ ОТЛАДКИ........................... 11.2.2. ВЫЯВЛЕНИЕ ОШИБКИ........................ 11.2.3. ЛОКАЛИЗАЦИЯ ОШИБКИ В ТЕКСТЕ ПРОГРАММЫ... 11.2.4. УСТАНОВЛЕНИЕ ПРИЧИНЫ ОШИБКИ............. 11.2.5. ИСПРАВЛЕНИЕ ОШИБКИ...................... 11.3. ОТЛАДЧИК TURBO DEBUGGER ДЛЯ WINDOWS.......... 11.3.1. ОБЩИЕ СВЕДЕНИЯ.......................... 11.3.2. УПРАВЛЕНИЕ ОТЛАДКОЙ..................... 11.3.3. ТОЧКИ ОСТАНОВА.......................... 11.3.4. ОКНА ОТЛАДЧИКА.......................... ПРИЛОЖЕНИЯ........................................... А. МОДУЛИ BORLAND PASCAL ДЛЯ WINDOWS.............. Б. СИСТЕМНЫЕ МЕТРИКИ WINDOWS....................... В. ВИРТУАЛЬНЫЕ КОДЫ КЛАВИШ........................ Г. ДИРЕКТИВЫ КОМПИЛЯТОРА........................... ИНДЕКС..............................................412
Справочное издание Сурков Дмитрий Андреевич Сурков Кирилл Андреевич Вальвачев Александр Николаевич Программирование в среде Borland Pascal для Windows Справочное пособие Редактор М.С. Молчанова Художник обложки и художественный редактор В.А. Ярошевич Технический редактор Г. М. Романчук Корректор Н.Б. Назарева
Подписано в печать с авторского оригинала-макета 7.09.95. Формат 60x90/16. Бумага книжно-журнальная. Гарнитура Таймс. Офсет, печать. Усл. печ. л. 27. Усл. кр. отт. 27,75. Уч.-изд.л. 32,14. Тираж 10000 экз. Заказ 1049. Издательство «Вышэйшая школа» Министерства культуры и пе- чати Республики Беларусь. Лицензия ЛВ № 5. 220048, Минск, проспект Машерова, 11. Отпечатано с оригинал-макета заказчика в типографии издательства «Белорусский Дом печати». 220013, Минск, пр. Ф. Скор ины, 79.
Сурков Д.А. и др. С90 Программирование в среде Borland Pascal для Windows: Справ, пособие / Д.А.Сурков, К.А.Сурков, А.Н.Вальвачев. - Мн.: Выш. шк., 1996.—432 с.: ил. ISBN 985-06-0162-0. Рассмотрена новейшая версия языка Паскаль - Borland Pascal для Windows. Технология разработки программ представлена “с нуля” до достаточно сложных Windows-приложений. Охвачены все основные аспекты программирования в Windows: управление событиями, интерфейс графических устройств GDI, разработка ресурсов с помощью Resource Workshop и их использование в программе, пользовательский интерфейс MDI, вывод на принтер, построение DLL-библиотек, технология drag-and-drop, передача данных между программами через буфер обмена и по протоколу DDE, отладка программ. Описана объектно-ориентированная библиотека OWL. Теоретический материал проиллюстрирован большим количеством примеров и рисунков. Для студентов высших и средних специальных учебных заведений, а также программистов, желающих разрабатывать приложения для Windows 6810300600 - 058 С------------------БЗ 98 - 95 ББК 22.18я2 М304(03)-96
Книга написана для студентов и программистов, желающих научиться быстро и качественно создавать программы для Windows. Материал обсуждался с рядом ведущих программистов США (Borland Int, Европейский филиал в Брюсселе), Англии (Wrox Press), Голландии (Square, официальный поставщик ПО для Пентагона) с целью выбора наиболее актуальной для современного программирования тематики. Примеры программ тщательно подобраны и протестированы. Методика подачи материала позволяет даже новичку выйти на профессиональный уровень программирования и подготовиться для работы в фирмах, создающих программное обеспечение. ISBN 985-06-0162-0