Технология Клиент-Сервер 2001'1 |
|||||||
|
Алексей Эксаревский, ave@relex.ru
Виталий Максимов, vitamax@relex.ru
В связи с ростом популярности ОС Linux значительно вырос спрос на приложения, ориентированные на работу с этой операционной системой. Соответственно, стала актуальной проблема выбора средства разработки таких приложений. К сожалению, большинство подобных систем ориентировано на работу в Windows.
В этом году отечественная компания РЕЛЭКС (www.relex.ru) выпустила собственное средство разработки прикладных систем – Linter Application Builder (LAB), с помощью которого можно создавать приложения как для семейства ОС Windows, так и для различных версий UNIX и LINUX.
LAB (Linter Application Builder) – это переносимое средство для быстрой разработки прикладных программ с графическим интерфейсом пользователя, в особенности клиент-серверных приложений, ориентированных на обработку данных с использованием системы управления базами данных ЛИНТЕР или других СУБД, а также (в перспективе) WEB-приложений. Основной целью разработки являлось создание переносимого средства быстрой разработки приложений (RAD) для СУБД ЛИНТЕР. Тем не менее, коммерческая версия LAB сможет работать и с другими системами управления базами данных на основе JDBC. На данный момент LAB является единственным конкурентом для системы Kylix фирмы Borland/Inprise, причем Kylix как коммерческий продукт пока не доступен, а в качестве его среды функционирования анонсируется только Linux (по крайней мере, о других платформах фирма Borland/Inprise не сообщает). В то же время, бета-версия системы LAB в настоящий момент работает на платформах Linux, Free BSD и SUN Solaris, и спектр поддерживаемых UNIX-сред будет расширяться.
Для построения приложений в LAB используется объектно-ориентированный компонентный подход, согласно которому приложение строится как иерархия имеющихся в распоряжении разработчика компонентов. Их набор может постоянно расширяться, как за счет компонентов, разработанных в самой системе , так и за счет компонентов сторонних разработчиков.
Стандартный набор компонентов включает различные визуальные компоненты, компоненты для работы с источниками данных (в частном случае – с СУБД Линтер), графические примитивы, компоненты для генерации отчетов. Настройка компонентов осуществляется визуально через их свойства. Для формирования логики выполнения приложения используется событийный механизм, основанный на возможности подписки обработчиков любого количества различных компонентов приложения на некоторое событие заданного компонента.
Для облегчения процесса разработки приложений в системе предусмотрен набор мастеров, позволяющих создавать требуемые сочетания взаимосвязанных компонентов.
Иерархия компонентов сохраняется в виде программы на объектно-ориентированном языке, которая далее может анализироваться для восстановления проекта. Для генерации и разбора кода приложения система использует абстрактные объекты, что позволяет сделать ее независимой от конкретного используемого языка. В текущей версии системы в качестве базового языка используется оригинальный язык АТОЛЛ, сочетающий возможности современных объектно-ориентированных языков, переносимость скомпилированного байт-кода и низкие требования к вычислительным ресурсам. В дальнейшем предполагается возможность использования в LAB языков Java и C++.
Система LAB представляет собой интегрированную среду разработки, включающую средства управления проектом, визуального создания и настройки компонентов, редактор исходного кода, средства компиляции, выполнения и отладки приложений, разнообразные мастера.
Система LAB состоит из следующих модулей:
Средства генерации отчетов реализованы как специальные компоненты стандартной библиотеки и набор мастеров.
Компоненты LAB характеризуются следующими общими свойствами:
Стандартный набор компонентов включает:
Система LAB состоит из двух основных систем: системы разработки приложений и системы исполнения приложений.
Система разработки позволяет создавать/сохранять/открывать проект прикладной системы, добавлять (удалять) в него компоненты, настраивать их свойства и создавать обработчики событий, выполнять соответствующие действия при помощи мастеров, вручную редактировать код программы, компилировать, запускать и отлаживать приложение.
Система исполнения предназначена исключительно для эксплуатации готовых приложений, скомпилированных из исходных текстов.
Чтобы кратко ознакомиться с базовыми возможностями системы LAB, рассмотрим пример создания с ее помощью простейшего приложения. Мы будем описывать необходимые шаги и попутно объяснять основные особенности LAB.
Пусть наше приложение должно выполнять следующие функции:
ввод в базу данных СУБД ЛИНТЕР и показ информации о некоторых людях (включая фамилию, имя, отчество, возраст и фотографию) и их автомобилях (марка, год выпуска, номер), причем у каждого владельца может быть ноль или более автомобилей;
формирование отчета об автомобилях, сгруппированных по их владельцам с выдачей в отчет информации из БД.
На примере создания данного приложения демонстрируются следующие основные операции при создании приложений в среде LAB:
работа с проектом (создание, сохранение, открытие);
создание компонентов;
установка и изменение свойств компонентов;
работа с БД: соединение с сервером БД, выполнение запросов, привязка результатов запросов к визуальным элементам формы; отработка связей между таблицами БД типа «один ко многим»; работа со справочниками; сохранение изменений в БД;
создание отчетов;
запуск мастеров;
создание меню;
обработка событий;
запуск приложения.
Пусть для хранения информации о владельцах и их автомобилях в БД имеются три таблицы, структура и связи которых приведены на рис 1.
Здесь символом <пк> помечен первичный ключ, а <вк> – внешний ключ. В таблице PEOPLE хранятся фамилии и фотографии людей, в таблице CARS – информация об их автомобилях, а таблица VEHICLE используется как справочник автомобилей (включает имя и внешний вид автомобиля).
Предположим, что эти таблицы существуют в базе данных СУБД ЛИНТЕР и их владельцем является пользователь SYSTEM.
Проект LAB представляет собой набор исходных файлов на языке АТОЛЛ, а также файл с перечнем имен этих файлов. Дополнительно может создаваться файл ресурсов для хранения используемых изображений. В принципе, этой информации достаточно для сборки приложения. Для удобства работы LAB дополнительно хранит дерево компонентов и классов во внутреннем, двоичном представлении. Этот файл используется при работе среды, но в случае его отсутствия LAB может сгенерировать его по исходным текстам.
Все перечисленные файлы по умолчанию помещаются в директорию проекта, которая задается при его создании.
Новый проект создается из главного меню LAB. В будущих версиях LAB можно будет указывать тип проекта, например WEB-приложение или динамически подключаемая библиотека. Пока доступен только один тип: обычное приложение.
После создания нового проекта (или открытия существующего) в LAB становятся доступны окно проекта с деревом форм, компонентов, классов и списком файлов, а также редактор исходных текстов, содержащий код основного класса нового приложения, автоматически сгенерированный средой. Этот код выглядит так:
//!! LAB project file //{{ Project definition: demo class demo : CApp //!! Component definition: ? virtual method main(); method CreateForms(); //!!**** New methods definition area implement method main() code CreateForms(); //!! Open main form Run(); end //!!**** New methods implementation area method CreateForms() code //{{ Create component: ? end end //}}
Это код класса demo, производного от стандартного класса приложения CApp, на языке АТОЛЛ. При запуске приложения автоматически создается один объект данного класса и вызывается его метод main. В тело метода main LAB помещает вызов метода CreateForms для создания форм и метода Run(), который инкапсулирует цикл обработки сообщений в программе. Комментарии вида //!! и //{{ … //}} используются LAB для быстрого разбора кода и не должны редактироваться пользователем.
После того, как проект создан, можно создать главную форму нового приложения (при помощи соответствующей кнопки в палитре компонентов). Созданной пустой форме можно придать необходимое расположение и размеры. Пример внешнего вида визуальной среды после создания формы приведен на рисунке 2.
Теперь объект, класс и файл с исходным текстом для формы Form0 доступны в Окне проекта, кроме того, открылось окно Инспектора объектов, позволяющее просматривать и изменять свойства компонентов, а также производить подписку на события.
Форма представляет собой контейнерный компонент, то есть компонент, способный содержать другие компоненты. Под контейнерные компоненты LAB создает свой собственный класс, переменные-члены которого предназначены для ссылок на дочерние компоненты. Код такого класса на языке АТОЛЛ выглядит следующим образом:
//{{ User class: Form0 class Form0 : CForm constructor (in parent CComponent; in name char(50)); //!! Component definition: ? virtual method CreateChilds(); //!!**** New methods definition area implement constructor (in parent CComponent; in name char(50)) code super(parent, name); CreateChilds(); end //!!**** New methods implementation area method CreateChilds() code //{{ Create component: ? end end //}}
Метод CreateChilds предназначен для создания дочерних компонентов. Его вызов помещен в конструктор, так что дочерние компоненты создаются автоматически при создании контейнера.
Форма, в свою очередь, является дочерней для приложения. Ссылка на форму сохраняется в переменной-члене класса приложения:
//!! Component definition: Form0 Form0 Form0;
Код, создающий объект-форму, помещается в метод CreateChilds класса-приложения:
method CreateForms() code //{{ Create component: Form0 Form0 := create Form0(NULL,"Form0") init( Text := "Form0", Left := 240, Top := 150, Width := 320, Height := 300, // . . . Menu := ""); //{{ Create component: ? end
Оператор create создает новый объект класса Form0, а конструкция init задает начальные значения переменных-членов, которые, по сути, являются свойствами формы. Ссылка на созданный объект сохраняется в переменной Form0.
В языке АТОЛЛ доступ ко всем объектам осуществляется через ссылки, а сами объекты обязательно должны быть созданы оператором create. АТОЛЛ ведет счетчик ссылок на объект, и когда все ссылки исчезают (переменные выходят из области видимости), объект автоматически удаляется, подобно механизму сборки мусора в языке Java. Однако, в отличие от Java, объект можно явно удалить при помощи оператора delete. В этом случае все остальные ссылки на него, если они были, получат значение NULL.
Подобная схема создания компонентов используется LAB не только для форм, но и для всех компонентов: под каждый компонент в классе его родительского компонента создается переменная-ссылка на объект соответствующего класса, а сам объект создается в методе CreateChilds.
Создание объекта дает доступ к его свойствам и методам, однако, оно не означает реального создания функционирующего компонента (например, окна или запроса к БД). Чтобы компонент начал функционировать, его необходимо активизировать (открыть). Открытие формы осуществляется методом Open, вызов которого для главной формы автоматически помещается в метод main приложения. При открытии формы открываются также все ее дочерние компоненты, дочерние компоненты этих компонентов и так далее.
Таким образом, приложение на LAB всегда создается динамически, как набор взаимосвязанных объектов. Визуальная среда лишь помогает сгенерировать соответствующий код.
Вся работа с БД осуществляется через одно или несколько соединений с сервером БД. Для создания соединения с БД ЛИНТЕР необходимо создать функциональный компонент CLinConnect (), поместив его в созданную форму. При создании соединения с БД запрашиваются параметры соединения (имя пользователя, пароль, сервер БД) и в случае успешного соединения компонент становится активным.
Любой компонент в форме активизируется мышью (при этом в Инспекторе объектов отображаются его свойства). Активный компонент можно переместить с помощью drag-and-drop и изменить его размеры.
Создание выборки из таблицы владельцев автомобилей
К соединению с БД может быть привязано любое количество компонентов-источников данных, выполняющих запросы к БД. В частности, для получения выборки из таблицы владельцев PEOPLE необходимо сначала создать компонент CStatement () , аналогично тому, как было создано соединение с БД. После того, как компонент создан, необходимо задать его свойства: привязать его к соединению и указать select-запрос на выборку данных. Свойства задаются в окне Инспектора объектов (рис. 3).
Для связи компонента Statement0 с соединением БД используется свойство connectName. Для установки этого свойства LAB активизирует окно, приведенное на рис. 4.
В данном окне отображается дерево компонентов приложения, из которого можно выбрать подходящий для связи компонент. В нашем случае необходимо выбрать двойным щелчком мыши LinConnect0.
В LAB многие свойства компонентов устанавливаются как ссылка на другой компонент определенного класса. Выбор компонента всегда осуществляется при помощи приведенного на рис. 4 окна. Причем компонент может ссылаться на любой другой компонент, даже если они находятся в разных формах и один из них еще не создан. Достигается это за счет того, что свойство-ссылка хранит путь к компоненту, который формируется способом, похожим на формирование пути к файлу в файловой системе. В качестве имен каталогов здесь выступают имена контейнерных компонентов, а в качестве имен файлов – имена дочерних компонентов. Имя компонента передается как параметр его конструктору.
При попытке установить ссылку на компонент, который еще не создан, нужный путь заносится в специальную очередь. Эта очередь проверяется при создании каждого компонента, и если оказывается, что на создаваемый компонент кто-то хотел сослаться ранее, данная ссылка автоматически активизируется. Наоборот, при закрытии компонента все ссылки на него деактивизируются и снова заносятся в очередь.
Вернемся к нашему источнику данных. После того, как выбрано соединение, необходимо ввести select-запрос на выборку данных. В общем случае запрос вводится в свойство SQLQuery. Однако в нашем случае необходим простой запрос на выборку всех данных из таблицы PEOPLE. Для этого достаточно просто ввести (или выбрать) имя таблицы в свойство MainTable. В этом случае LAB генерирует запрос типа select * from PEOPLE.
По умолчанию все изменения свойств, произведенные в Инспекторе объектов, сохраняются по специальной кнопке «Применить». При нажатии кнопки «Отмена» или при смене активного компонента все сделанные изменения свойств аннулируются. При помощи меню «Опции» пользователь может включить режим автоматического применения свойств после изменения в Инспекторе объектов.
Свойство Structure источника данных позволяет настроить его структуру, то есть имена и типы колонок, а так же связь с соответствующими полями запроса. Структура задается при помощи диалогового окна, приведенного на рис. 5.
По умолчанию структура источника данных автоматически строится по запросу. После этого можно задать для каждой колонки произвольное символическое имя, добавить не связанные с БД колонки или указать, что значение колонки должно вычисляться как формула. Дадим колонкам нашего источника данных символические имена «N», «Фамилия» и «Фото».
Изменения значений полей источника данных возможно либо из привязанных к нему визуальных элементов, либо программным способом. Все изменения сохраняются во внутреннем кэше источника данных, и могут быть либо отменены, либо сохранены в БД. Изменять можно только обновляемые поля БД, помеченные знаком "1" в структуре источника (в нашем примере поле SYSTEM.PEOPLE.ID автоинкрементное, а потому явно задавать его значение запрещено).
Источники данных LAB автоматически строят запросы на добавление, обновление и удаление строк в соответствующих таблицах. При этом возможно указание различных стратегий идентификации записи: в позиционном запросе (were current of cursor), по первичному ключу или по внутренним системным номерам кортежей. Эти варианты задаются свойством UpdateMode.
После того, как произведено соединение с БД и выборка данных из БД, мы можем создать в нашей форме визуальные элементы, отображающие выбранные данные.
Большинство визуальных компонентов имеет свойства dataSource и dataField. dataSource – это ссылка на компонент-источник данных, а dataField – символическое имя поля источника данных (или список полей). Если эти свойства установлены, компонент автоматически будет отображать данные из источника, а изменения, сделанные пользователем в компоненте, будут попадать обратно в источник данных.
Пусть необходимо отобразить данные о владельцах автомобилей в табличном виде. Для этого используется компонент CGrid () cо страницы «Визуальные компоненты» палитры компонентов.
После того, как компонент Grid0 создан, его можно привязать к источнику данных (компоненту Statement0) при помощи свойства dataSource. Здесь используется стандартное окно ссылки на компонент. После привязки к источнику данных в компоненте Grid0 отобразятся данные из таблицы PEOPLE. Поле dataField используется для настройки количества и параметров колонок таблицы. Размеры колонок компонента Grid0 можно настроить визуально. Каждая колонка имеет дополнительные свойства, которые можно изменить по двойному щелчку мышью на заголовке колонки.
Для показа фотографий людей из БД можно создать графический компонент CImage (). Этот компонент необходимо привязать к источнику данных Statement0 так же, как это делалось для Grid0. После привязки необходимо выбрать поле, отображаемое компонентом. Это делается при помощи свойства dataField. Теперь графический компонент начинает показывать изображения из БД (рис. 6). Мы можем изменять текущую строку в табличном элементе grid0, при этом в image0 будет меняться фотография человека. Элемент CImage способен отображать изображения из BLOB-полей в форматах JPEG, BMP, GIF и PNG.
Последовательность, когда сначала создается источник данных, а затем – компоненты для визуализации данных, не является обязательной. Можно сделать и наоборот: сначала создать визуальные компоненты, затем – соединение с БД и источник данных, а уже потом привязать визуальные компоненты к источнику данных. В нашем случае описанный способ просто является более логичным, однако в случае редактирования существующих приложений возможна привязка и «перепривязка» визуальных элементов к различным источникам данных.
После того, как получена форма для показа данных о владельцах автомобилей, можно добавить новый источник данных, предназначенный для выборки информации об автомобилях по таблице CARS и привязать к нему еще один табличный элемент. Вместо компонента CStatement теперь лучше взять CSmartStatement (). Отличие этого компонента состоит в том, что при использовании в качестве подчиненного он способен кэшировать изменения для разных строк главного источника данных одновременно. Пользователь может свободно перемещаться по главной выборке, видя все изменения, сделанные им в подчиненной выборке, без реальной модификации БД. Все изменения потом могут быть разом внесены в БД, либо отменены.
Поскольку название автомобиля хранится в справочной таблице VEHICLE, а в таблице автомобилей CARS хранятся только коды, нам придется использовать SQL-запрос на выборку данных из этих двух таблиц:
select name, cars.* from cars, vehicle where cars.idcar=vehicle.id;
Этот запрос пишется непосредственно в окне для свойства SQLQuery. Следующая версия LAB будет включать возможность графического построения запросов.
Теперь в свойстве MainTable необходимо выбрать таблицу SYSTEM.CARS, поскольку это именно та таблица, в которую мы намерены заносить сделанные изменения, а таблица SYSTEM.VEHICLES является справочной. Если свойство MainTable не задано, источник данных не будет заносить никаких изменений в БД.
Присвоим колонкам в источнике данных SmartStatement0 имена «Марка» (VEHICLES.NAME), «Год» (CARS.YEAR) и «Пробег» (CARS.MILES). Это те колонки, значения которых мы хотим показывать. Остальные колонки (CARS.ID, CARS.IDCAR, CARS.IDP) необходимо было включить в выборку, так как соответствующие коды должны быть занесены в таблицу CARS (формировать их будут сами компоненты, без нашего участия).
Теперь под компонентом Grid0 можно создать еще один табличный компонент, Grid1, связанный с источником данных SmartStatement0.
По умолчанию табличный элемент (CGrid) показывает все столбцы источника данных. Чтобы ограничить количество показываемых столбцов, используется свойство dataField. В диалоговом окне для dataField компонента Grid1 можно удалить колонки CARS.ID, CARS.IDCAR, CARS.IDP из числа отображаемых.
Данные об автомобилях в нашем случае показываются независимо от данных о владельцах. Чтобы связать их вместе, используется специальный связующий компонент CStmtLink () из раздела «Работа с БД» палитры компонентов. Создадим этот компонент и установим в Инспекторе объектов в качестве свойства PrimaryStatement "Statement0", а в качестве свойства SecondaryStatement "SmartStatement0". Чтобы создать условие связи в Инспекторе объектов активизируется окно для установления свойства Fields (рис. 7). В данном окне задается соответствие ключевых полей главного и подчиненного источников данных (в нем может участвовать несколько полей – составные ключи). В текущей версии LAB это соответствие устанавливается вручную. В следующей версии системы планируется добавить средства автоматического анализа ограничений ссылочной целостности в БД.
После установки свойства Fields связующего компонента источники данных Statement0 и SmartStatement0 начинают работать в режиме главный-подчиненный, так что при изменении текущей строки в таблице владельцев, в таблице автомобилей автоматически показываются только автомобили данного владельца.
Связующие компоненты можно использовать для связи произвольных двух источников данных, в том числе уже участвующих в других связях. При этом образуется сложная разветвленная структура связанных источников данных, в общем случае имеющая вид графа. Причем все связи отрабатываются компонентами автоматически, без дополнительного программирования. Имеется механизм защиты от зацикливания связей.
В нашей форме марки автомобилей показываются правильно, однако, не хватает механизма для выбора автомобиля из справочной таблицы VEHICLES. Этот механизм обеспечивается на основе компонента – «словаря» CDict (). Основа функционирования словаря следующая. Задается произвольный запрос, который используется для показа значений в списке, из которого пользователь может сделать свой выбор (запрос на выборку). Затем выбирается источник данных (dataSource), с которым работает словарь. Когда пользователь выбирает некоторую запись из словарного списка, часть соответствующих полей из запроса на выборку копируется в заданные поля dataSource. Соответствие полей задается отдельным свойством Fields.
Создадим словарь Dict0, выберем в качестве connectName наш LinCinnect0, в запросе на выборку SQLQuery напишем "select name, id from vehicle;" (в выпадающем списке справочных значений показываются колонки из запроса в том порядке, который задан в запросе), в свойстве Structure назовем VEHICLE.NAME символическим именем «Марка», а VEHICLE.ID – именем «Номер». В качестве dataSource выберем SmartStatement0, а в свойстве Fields зададим соответствие полей: "CARS.IDCAR"="Номер" и "Марка"="Марка". Первое соответствие обеспечивает занесение кода выбранного автомобиля в источник данных SmartStatement0. Этот код потом может быть сохранен в БД в таблице CARS. Второе соответствие обеспечивает визуальное отображение марки выбранного автомобиля.
После того, как компонент-словарь создан, к нему можно привязать колонку таблицы Grid1. Для этого необходимо дважды щелкнуть по заголовку колонки и при помощи специальной кнопки выбрать словарь Dict0 (рис. 8).
После того, как колонка привязана к словарю, в ячейках для ввода марки автомобиля появляется кнопка, по нажатию которой появляется список справочных значений (согласно запросу, указанному в словаре). Вид данного окна приведен на рис. 9.
В стандартном окне выбора словарного значения поддерживается функция контекстного поиска значения, если эти значения отсортированы. Сортировка выполняется автоматически по соответствующему полю, если у словарной колонки установлен признак автосортировки, поэтому мы не указали order by в запросе на выборку.
Кроме колонки таблицы, к словарю можно также привязать выпадающий список (combobox).
LAB позволяет вместо стандартного окна выбора словарного значения использовать свое собственное. Для этого создается отдельная форма, ссылка на которую указывается в свойстве RefForm словаря. Форма получает доступ к словарю через специальный компонент – формальный параметр, так что одну и ту же форму выбора можно использовать для разных словарей. В нашем примере мы могли бы разработать форму, позволяющую выбирать автомобили из иерархического списка, отображая дополнительно их изображения.
Созданная в предыдущем пункте форма пригодна не только для просмотра информации из базы данных, но и для ввода информации, так как источники данных способны отображать на таблицу, которая указана в свойстве mainTable, все изменения. Например, можно изменять данные, которые показываются в табличных компонентах Grid0 и Grid1, прямо в этих компонентах. Как уже было сказано, изменения кэшируются компонентами. Для того чтобы данные были реально внесены в БД, необходимо вызвать соответствующий метод источника данных. Еще один метод источника данных позволяет наоборот, сбросить информацию в кэше и обновить источник данных в соответствии с содержимым базы данных.
Кроме того, любой компонент-источник данных имеет методы, позволяющие добавлять в таблицу новые или удалять ранее внесенные строки.
Таким образом, в нашей форме не хватает только механизма вызова перечисленных методов. В простейшем случае этот механизм обеспечивается компонентом-навигатором CDBNavigator (). Этот компонент имеет фиксированное число кнопок, предназначенных для выполнения перечисленных функций, а так же для навигации по записям источника данных. Часть кнопок может быть выключена при помощи специальных свойств.
Добавим компонент-навигатор в нижнюю часть нашей формы. Чтобы он работал, его необходимо связать с источником данных при помощи свойства dataSource, так как это делалось ранее для визуальных компонентов. Связав навигатор со SmartStatement0, мы получаем возможность внесения изменений в таблицу автомобилей. Причем при добавлении новых записей связующий компонент StmtLink0 обеспечивает занесение в поле "CARS.IDP" идентификатора текущего владельца, поэтому новая запись оказывается связанной именно с данным владельцем, как и требовалось. Отметим еще раз, что для фиксации изменений в БД необходимо нажать кнопку навигатора для сохранения изменений.
Для того чтобы реализовать возможность изменения данных в таблице PEOPLE, можно добавить в форму еще один навигатор, связав его с источником данных Statement0.
Меню представляет собой совокупность иерархически организованных компонентов CMenuItem, расположенных внутри функционального компонента CMenu. Так как эта структура достаточно громоздка, для создания меню используется мастер меню, позволяющий визуально создавать и редактировать меню (рис. 10).
Для того чтобы меню отобразилось в форме, необходимо связать форму с данным меню. Для этого используется свойство Menu формы, которое является ссылкой на меню. Одно и то же меню может использоваться в нескольких формах. При установке ссылки на меню оно автоматически отображается в форме.
Окончательный вид формы с меню приведен на рис. 11.
Чтобы меню заработало, необходимо создать соответствующие обработчики события выбора его пунктов. Допустим, мы хотим, чтобы по пункту «Выход» осуществлялся выход из приложения.
Для этого можно щелкнуть мышью по нужному пункту меню в форме. В результате появится окно подписки на событие (рис. 12).
Окно подписки позволяет связать один метод любого контейнерного компонента приложения с данным событием. Под каждый контейнерный компонент создается отдельный класс. Любой класс может содержать метод, который можно связать с событием.
Чтобы подписать метод компонента на событие, в данном окне выбирается соответствующий элемент дерева объектов и в поле ввода вверху окна вводится имя метода-обработчика (по клавише Enter или двойному щелчку мыши вставляется имя, генерируемое средой по умолчанию). Имя обработчика для каждого объекта можно выбрать из списка уже имеющихся в этом объекте методов с подходящими под событие параметрами. Таким образом, осуществляется связка одного обработчика с несколькими событиями.
Имена обработчиков можно в дальнейшем изменять, сделав активным нужный элемент дерева и отредактировав поле ввода. В этом же окне аннулируется подписка на событие.
В нашем случае удобно, чтобы событие от пункта меню «Выход» обрабатывалось формой, поэтому необходимо щелкнуть мышью по строке, представляющей форму. По двойному щелчку мышью в поле ввода вставляется имя метода-обработчика по умолчанию: OnMenuItem1Select. Повторное нажатие ENTER или двойной щелчок мышью позволяет активизировать редактор исходного текста программы и переместить курсор в начало метода OnMenuItem1Select, созданного для обработки события.
Теперь в строке между ключевыми словами "begin" и "end" можно вставить вызов метода "CloseWin();", обеспечивающий закрытие формы при выборе пункта меню а, следовательно, и выход из пользовательского приложения, так как приложение на LAB автоматически завершается при закрытии главной формы.
Полный список событий любого компонента доступен в Инспекторе объектов на вкладке События. Набор событий визуальных компонентов LAB включает нажатия различных кнопок мыши, нажатие клавиш, перемещение мыши, изменение фокуса ввода, события для поддержки darg-and-drop и так далее. Функциональные компоненты имеют свой перечень событий. Например, у компонента-таймера есть событие tick, вызываемое таймером через заданные интервалы. Подписка на любое событие осуществляется в стандартном окне подписки.
Оформление подписки на событие в исходном тексте приложения происходит путем вставки вызовов методов SubscribeForMsg и SubscribeComponent в коде создания компонента-подписчика и компонента-издателя соответственно. Дублирование вызовов необходимо в ситуации, когда на момент создания одного из компонентов второй еще не создан. В этом случае указатель на него равен NULL, и вызов метода подписки ничего не делает. И только при создании второго компонента соответствующий метод зафиксирует подписку. Библиотека компонентов поддерживает уникальность подписок, поэтому повторная установка одной и той же подписки никак не влияет на работу приложения. Подписка в приложении будет аннулирована, как только будет уничтожен один из компонентов.
Пример кода, генерируемого для подписки на события:
Главный файл проекта:
method CreateForms() code // . . . Form0.SubscribeForMsg(CMenuItem::Select,"Form0/Menu0/MenuItem1", "OnMenuItem1Select"); // . . . end
Файл с классом меню:
method CreateChilds() code // . . . MenuItem1.SubscribeComponent(CMenuItem::Select, "..", "OnMenuItem1Select"); // . . . end
Таким образом, при подписке указывается идентификатор события, путь к компоненту-источнику или к компоненту-обработчику, и имя метода-обработчика.
Простое приложение для просмотра и ввода информации в связанных таблицах PEOPLE, CARS и VEHICLE готово. Запустить его можно при помощи соответствующих команд среды LAB. Вначале исходные тексты на языке АТОЛЛ компилируются во внутренний байт-код. В случае каких-либо ошибок в коде будут выданы сообщения об ошибках с возможностью позиционирования по местам обнаружения ошибок.
После успешной компиляции запустится программа исполнения готового приложения labdbg, которая включает виртуальную машину АТОЛЛ. Виртуальная машина использует байт-коды классов, помещенные компилятором в отдельные файлы с расширением pc. Эти файлы переносимы между платформами Windows и Unix. При запуске приложения на экране появится разработанная нами форма и диалог соединения с БД.
После успешного соединения мы можем наблюдать работу нашего приложения. Необходимо отметить, что все функции приложения по просмотру и модификации БД могли выполняться и в режиме разработки формы. Для облегчения процесса разработки приложений в LAB реализована возможность функционирования всех компонент в полном объеме уже на этапе разработки. Естественно, что на этом этапе не может выполняться написанный вручную программный код. В нашем случае этот код минимален: это обработчик события меню, осуществляющий закрытие формы. Проверить его работу можно, просто выбрав пункт меню «Выход». При этом форма закроется, и приложение завершит свое выполнение.
LAB также позволяет отлаживать приложение, выполняя его по шагам, устанавливать точки останова, просматривать переменные и т.д. Отлаживаемое приложение выполняется программой labdbg, которая запускается непосредственно из-под LAB и устанавливает тесное взаимодействие с системой для реализации отладочных функций. Эта программа не может запускаться отдельно от средства разработки.
Пример окна редактора в процессе отладки приведен на Рис. 13.
На данном рисунке изображен момент отладки, когда произошла остановка на одной из точек останова (точки останова можно устанавливать/сбрасывать как до запуска приложения, так и в процессе его работы). В этом состоянии в отладчике можно просматривать иерархию объектов приложения, локальных переменных и их значения (в виде дерева), стек вызова методов, а также значения произвольных выражений (на рисунке 13 окно наблюдения за значениями выражений вынесено отдельно).
После того, как произошел останов в отладчике, можно выполнить один следующий оператор с заходом внутрь метода (если выполнение оператора предполагает вызов метода) или без захода; можно продолжить выполнение программы до выхода из текущего метода, до завершения приложения или до следующего останова. Останов может произойти по точке останова или по исключению. Останов по исключению позволяет установить причину исключения, проанализировав значения переменных и стек вызова.
Для запуска готового приложения существует программа labrt, выполняющая приложение без режима отладки. Командная строка для ее запуска имеет вид:
labrt [-d <путь>] <имя приложения>
где необязательный параметр –d <путь> задает путь к директории, в которой находятся скомпилированные байт-коды проекта (файлы с расширением .pc), а <имя приложения> – это имя главного класса приложения (LAB делает его таким же, как имя проекта, указанное пользователем при его создании).
Для получения отчета по владельцам и их автомобилям необходимо разработать отдельную форму. Форма в данном случае является носителем для компонента-отчета и для источника данных, по которому отчет генерируется.
Создадим форму Form1, поместим в нее компонент-отчет Report0 (класс CReport ) и источник данных Statement1.
При размещении визуальных компонентов внутри своего родителя они выравниваются по сетке заданного размера. Кроме того, дополнительные свойства компонента позволяют задать опции выравнивания внутри клиентской области родителя: компонент может быть прижат к одной из четырех сторон или занимать оставшуюся клиентскую область. Ширина (высота) прижатого компонента может быть задана как в пикселях, так и в процентах относительно клиентской области. В последнем случае при изменении размеров родителя размеры дочернего компонента будут автоматически подстраиваться. Более продвинутый способ выравнивания позволяет задать компонент CGridPanel, который разбивается сеткой, а для каждого дочернего компонента задаются занимаемые им ячейки сетки и его выравнивание внутри ячейки.
В нашем случае удобно прижать компонент-отчет к верхней части формы (свойство Alignment=Top) и указать выравнивание в процентах (AlignentUnit=Percents). Тогда при изменении размеров формы компонент-отчет будет соответствующим образом подстраиваться, оставляя внизу область с источником данных.
Чтобы сформировать отчет, необходимо выполнить соответствующий select-запрос на выборку информации из БД. Нам необходима следующая информация: имя и фотография владельцев из таблицы PEOPLE, марка, год выпуска и пробег автомобиля из таблиц CARS и VEHICLE. Информация должна быть сгруппирована по владельцам. Компонент CReport способен сам поддерживать группировку по некоторому полю, считая группой ряд записей, для которых значения этого поля одинаковы. Поэтому, чтобы добиться группировки по имени владельцев необходимо отсортировать ответы по полю PEOPLE.NAME. Таким образом, для нашего отчета необходим следующий запрос:
select people.name,people.photo,vehicle.name,year,miles from people,cars,vehicle where cars.idp=people.id and vehicle.id=cars.idcar order by 1;
Этот запрос заносится в свойство SQLQuery источника данных Statement1, после того, как он привязан к соединению LinConnect0 из главной формы.
Полученная структура источника данных может использоваться для формирования необходимой структуры отчета. Для этого отчет вначале связывается с источником данных (через свойство dataSource). Затем структура отчета задается в диалоге, соответствующем свойству Structure (рис. 14).
В наш отчет мы включаем заголовок и конец страницы, а также заголовок и конец группы по полю PEOPLE.NAME. В результате в компоненте-отчете появляются соответствующие области, в которых мы и разрабатываем форму отчета (рис. 15). Размер любой области можно изменить визуально.
Форма отчета разрабатывается, в основном, при помощи компонентов двух видов: графических примитивов (прямоугольник и линия) для изображения рамок, линий и визуальных элементов, привязанных к источнику данных.
Графические примитивы можно проводить через несколько областей сразу, что облегчает разработку таблиц.
Компоненты, привязанные к источнику данных, удобно создавать при помощи Мастера связи с источником данных (рисунок 16).
Мастер позволяет выбрать источник данных, его поле, зафиксировать кнопку с изображением соответствующего компонента, и создать этот компонент в форме. Это быстрее, чем вначале создать визуальный компонент, а уже затем привязать его к источнику данных при помощи свойств dataSource и dataField. Кроме того, мастер позволяет наглядно просматривать, с каким источником связан активный визуальный компонент, и, при необходимости, изменять эту связь.
Для нашего отчета выполним следующее:
очертим рамку таблицы при помощи прямоугольника, проведя его сквозь все области отчета;
создадим несколько вертикальных и горизонтальных линий-разделителей;
подпишем колонки в заголовке страницы при помощи компонентов CLabel;
при помощи Мастера связи с источником данных вставим в тело отчета поля, связанные с VEHICLE.NAME, CARS.YEAR и CARS.MILES, а в заголовок группы – поле для фамилии человека (PEOPLE.NAME) и его фотографии (PEOPLE.PHOTO) (у фотографии включим свойство stretch, чтобы картинка масштабировалась);
подправим размеры областей отчета.
В результате форма получит вид, приведенный на рис. 17.
Компонент-отчет предоставляет удобный интерфейс для изменения размеров уже разработанной формы: на верхнем или нижнем служебном поле можно нажать левую кнопку мыши (при этом появляется вертикальная или горизонтальная линия) и, не отпуская ее, потянуть линию в нужную сторону. Когда кнопка мыши будет отпущена, все компоненты находящиеся правее (ниже) линии будут сдвинуты, а компоненты, которые линия пересекала, будут увеличены или уменьшены. Таким образом, можно, например, увеличить или уменьшить размер колонки таблицы, зарезервировать место под новую колонку и так далее, не нарушая при этом целостности формы отчета. Эта возможность позволяет сэкономить достаточно много времени при модификации готовых отчетов.
Дополнительно мы можем вставить в отчет номер страницы и поле для получения суммарного пробега автомобилей. Эти возможности предоставляет Мастер связи с источником данных, когда активен компонент-отчет.
Для вывода в отчете номеров страниц расширим область заголовка страницы, выберем в Мастере связи с источником данных радиокнопку "Номер страницы", зафиксируем в верхней части кнопку и щелкнем мышью в заголовке отчета там, куда хотим поместить номер страницы.
Поле для суммы вставим в область конца группы PEOPLE.NAME, выбрав в Мастере связи с источником данных радиокнопку "Сумма". По кнопке "Источник" выберем визуальный компонент, значения которого мы хотим суммировать. Например, если необходимо просуммировать пробег автомобилей, а соответствующие данные выводятся в поле Edit4, необходимо выбрать Edit4 в иерархии компонент. LAB позволяет суммировать значения, выводимые в любом визуальном компоненте, по группе, странице или отчету. Для выбора типа суммы используется выпадающий список «Группа», в котором выберем "PEOPLE.NAME" для суммирования по соответствующей группе. Теперь можно создать соответствующее поле, в которое будет выводиться сумма. Слева поставим метку с текстом «Итого».
На самом деле при вставке сумм и номеров страницы Мастер связи с источником данных создает обычный визуальный элемент, не привязанный ни к какому источнику данных. Чтобы отобразить нужные значения в этом элементе, мастер дополнительно создает в классе отчета метод – обработчик события ReportBegin или PageBegin в тело которого вставляет необходимый код.
Для получения сумм соответствующий код вставляется в обработчик события ReportBegin. Например, чтобы отображать в компоненте edit10 сумму поля edit4 по группе, сформированной по полю PEOPLE.NAME, будет вставлен код
defineSum(Edit10, Edit4, "PEOPLE.NAME", VerticalSum, 0);
(два последних параметра задают вертикальное суммирование и количество знаков после запятой). Эта функция задает генератору отчетов необходимость суммирования данных.
Для отображения номера страницы в обработчик события PageBegin вставляется код, аналогичный такому:
Edit11.Text := toString(getPageNumber());
Ясно, что при вставке сумм и номеров страницы эти поля будут работать правильно только при исполнении приложения. Изначально мастер просто устанавливает таким компонентам текст, позволяющий определить, что должно отображаться в них.
Чтобы величины пробега автомобилей и их суммы лучше читались, можно числа поразрядно выровнять вправо. Для этого можно использовать свойство Pattern компонента CEdit, позволяющее задать шаблон ввода и отображения данных. Шаблон может включать символы-разделители, указание различных цветов для положительных, отрицательных и нулевых значений, представление NULL-значений. В нашем случае используем простой шаблон "#######", означающий семь десятичных цифр с выравниванием числа вправо. Для задания шаблона используется специальное диалоговое окно. Кроме шаблона зададим этим двум полям также какой-нибудь моноширинный шрифт (свойство Font), например, Courier New, чтобы разряды чисел всегда находились один над другим. Для задания одинакового значения некоторого свойства сразу нескольким полям LAB позволяет выделить эти поля во множественный выбор, и затем изменить общие свойства.
Дополнительной возможностью при создании формы отчета является задание условия на печать того или иного компонента, которое позволяет задать Мастер условий.
Отчет имеет несколько событий, которые вызываются при печати различных областей. В обработчиках этих событий можно реализовывать дополнительную логику. В частности, мастера связи с источниками данных и условий используют эти события.
Кроме свойств dataSource и Structure отчет имеет свойства crossDataSource и crossStructure, которые задают второй источник данных и вторую, вертикальную структуру деления отчета на области. В результате совмещения dataSource, crossDataSource и горизонтального деления с вертикальным, получаем отчет, способный формировать таблицу с переменным числом столбцов и строк (так называемую «шахматку»).
Некоторые визуальные компоненты, включенные в отчет, способны при печати разворачиваться в высоту в зависимости от данных. Это список, таблица и многострочное поле ввода. Последнее можно использовать для отображения в отчете длинных строк, когда необходимо, чтобы длинные строки переносились.
Для того чтобы наше приложение могло формировать отчет по разработанной форме, необходимо вызвать метод Print() для формы отчета. Сделать это можно, например, из специального пункта меню главной формы.
Добавим в меню «Файл» пункт «Отчет» (вызвав Мастер меню по двойному щелчку на компоненте-меню) и подпишем на него нашу форму отчета Form1. Вставим в тело метода-обработчика вызов метода Print(), который и обеспечивает генерацию отчета.
Теперь можно запустить наше приложение.
При выборе пункта меню «Отчет» будет запущен процесс генерации отчета (в специальном окне выводится состояние процесса и номера формируемых страниц) и появится окно предварительного просмотра результатов отчета (рис. 18), в котором будет выведена информация обо всех людях, владеющих автомобилями, и их автомобилях. При формировании отчета он разбивается на страницы таким образом, чтобы не разрывать его области.
Отчет из окна предварительного просмотра можно распечатать на принтере или экспортировать в формат HTML. LAB включает средства экспорта в HTML, позволяющие получить точную копию произвольной формы отчета. Пример результата экспорта в HTML приведен на рис. 19. Такой HTML-файл способен корректно отображаться браузерами Internet Explorer и Netscape версий 4.0 и выше.
Следующая версия LAB будет включать также средства экспорта отчетов в форматы RTF и PDF. Генерация отчета в том или ином формате может быть запущена, минуя предварительный просмотр, при помощи соответствующих методов.
Язык АТОЛЛ ориентируется на традиционные концепции объектно-ориентированного программирования. Основой языка АТОЛЛ являются классы, которые могут включать данные и методы, имеющие различные области видимости из других классов (protected – видно в данном и производном классах, public – видно всеми, published – как public, но с возможностью получения информации о таких членах класса извне программы, например, средой LAB). Классы могут наследоваться (множественного наследования нет), при этом реализуется механизм вызова виртуальных методов. Для каждого класса может быть задан один конструктор и один деструктор.
Язык АТОЛЛ имеет интерфейсы, позволяющие реализовывать внешние классы на языках C/C++. Эти классы могут быть как базовыми для других классов, так и унаследоваными от других классов, независимо от того, реализованы ли эти классы на языке C/C++ или на самом АТОЛЛ’е. Именно этот интерфейс использует LAB для подключения стандартных компонентов и внешних пользовательских разделяемых библиотек.
АТОЛЛ поддерживает следующие типы данных:
SMALLINT – целое 2 байта;
INTEGER – целое 4 байта;
REAL – число с плавающей точкой 4 байта;
NUMERIC – число с плавающей точкой 8 байт;
DECIMAL – число с фиксированной точкой (будет реализовано в ближайшей версии);
LOGICAL – логическое;
CHAR([<размер>]) – строка символов заданного или переменного размера;
DATE – дата и время;
( <список идентификаторов> ) – перечислимый тип; здесь <список идентификаторов> – это одно или несколько имен констант, разделенных запятыми. Константы получают значения 0, 1, 2, 3,... в порядке следования;
MASK ( <список идентификаторов> ) – битовая маска; здесь <список идентификаторов>- это одно или несколько имен констант, разделенных запятыми; константы получают значения 1, 2, 4, 8,... в порядке следования (степени двойки);
классы;
массивы с элементами любого из перечисленных типов.
АТОЛЛ позволяет присваивать переменной любого типа значение NULL и сравнивать ее с NULL. Таким образом, NULL совместим с любым типом.
Допустим, мы хотим разработать на LAB простенький редактор графов. Это приложение должно позволять создавать новые узлы графа (отображая их кружочками) и соединять их линиями – связями. С каждым узлом могут ассоциироваться некоторые дополнительные данные. Дополнительно наше приложение должно уметь распечатывать полученный граф. Вопросы сохранения и считывания графа с диска мы рассматривать здесь не будем, чтобы не перегружать пример.
Подобный граф логично представить в виде двух массивов: один содержит объекты – узлы графа, а второй – связи между ними (ссылки на первый и второй связанный элемент в первом массиве).
В качестве объекта-узла удобно использовать компонент CEllipse из стандартной библиотеки LAB, а в качестве связей – CLine. Удобство здесь в том, что эти компоненты будут посылать соответствующие события при нажатиях/перемещениях мыши над ними, и нам не придется каким-либо специальным способом учитывать их сложную форму.
В качестве рабочей области (контейнера), в которой мы будем рисовать граф, удобнее всего выбрать компонент- отчет CReport. Так как CReport умеет печатать все визуальные компоненты, расположенные в нем, то наш граф будет печататься автоматически одним вызовом метода Print!
Итак, приступим к созданию нашего приложения.
Создадим новый проект (например, с именем graph) и главную форму. Пусть форма содержит панель инструментов вверху и рабочую область (то есть компонент класса CReport) в оставшейся части.
Чтобы сделать панель инструментов, создадим в форме панель и установим ей выравнивание по верхней части (значение Top свойства Alignment). Затем создадим в ней две кнопки (чтобы нормально расположить их внутри панели, необходимо уменьшить размер сетки: свойство gridSize панели установить, например, в 5). Теперь кнопкам можно очистить текст, установить картинки (свойство NormalImage) и для красоты сделать их "всплывающими" (включить свойство isFloat).
После того, как панель инструментов готова, создадим рабочую область (компонент CReport). Установим ему размеры по оставшемуся размеру формы (значение Client свойства Alignment) и расширим границы области печати, потянув мышью за боковую и нижнюю части. Пусть компонент-рабочая область называется Canvas, соответствующий класс CCanvas, а файл Canvas.atl
Основа формы редактора графов готова (рис. 20).
Узел графа, по сути, представляет собой визуальный объект CEllipse, несущий дополнительную информацию о данных, ассоциированных с узлом, и связях этого узла с другими узлами. Эта информация о связях окажется полезной, когда мы будем перемещать узел графа. Переместив узел, надо подкорректировать координаты линий связи так, чтобы они по-прежнему входили и выходили из узла.
Таким образом, необходимо произвести от CEllipse новый класс (назовем его CNode), включив в него соответствующие переменные-члены. Пусть данными, которые ассоциируются с узлом, является просто текст, а связь между узлами описывается классом CNodeLink. Тогда код класса CNode на языке АТОЛЛ будет выглядеть следующим образом:
class CNode : CEllipse txt char(); // ассоциированный с узлом текст const maxLinks=20; // максимальное число связей links_From CNodeLink array[maxLinks]; // массив связей от узла links_To CNodeLink array[maxLinks]; // массив связей к узлу n_links_from, n_links_to int; // количество связей constructor(in parent CContainer; in name char()); implement constructor(in parent CContainer; in name char()) code super(parent, name); n_links_from := n_links_to := 0; end end
Мы будем отдельно хранить связи, выходящие из узла и входящие в него (это, в принципе, позволяет формировать направленный граф). Для хранения связей мы использовали массивы фиксированного размера, который задается константой maxLinks. На самом деле АТОЛЛ позволяет динамически создавать массивы нужной длины, и мы могли бы написать код так, чтобы массивы могли "расти" по мере необходимости. Однако для простоты не будем рассматривать здесь эту возможность.
Мы определили для класса CNode конструктор с параметрами, стандартными для компонента LAB: ссылка на родителя и имя компонента. В реализации этого конструктора вызывается конструктор базового класса (АТОЛЛ не вызывает их автоматически) и обнуляются счетчики ссылок.
Класс CNodeLink (связь узлов) производится от стандартного класса CLine и не содержит ничего, кроме двух ссылок на связываемые узлы:
class CNodeLink : CLine first, sec CNode; implement end
Код классов CNode и CNodeLink можно поместить в отдельный файл, добавив его в проект, а можно и добавить в начало или конец существующего файла, например файла Canvas.atl, сгенерированного LAB для компонента-рабочей области. Как видно, классы могут ссылаться друг на друга. При этом они располагаются в исходных файлах произвольно. Компилятор АТОЛЛ сам находит код нужного класса, когда это необходимо.
Создадим в классе CCanvas обработчик события нажатия кнопки, предназначенной для создания нового узла. Назовем этот обработчик NewNode:
method NewNode(in sender CComponent) result bool declare node CNode; code /* Создать новый объект и установить нужные свойства */ node := create CNode(this,"Ellipse"+tostring(n_nodes)) init( Left := xPos + 20, Top := yPos + 20, Width := 20, Height := 20, BrushColor := "128 128 128 0", PenColor := "0 0 0 0", ToolTipText := "Node", ShowToolTip := true, Transparent := false); /* Подписать нужные методы на"мышиные" события*/ node.SubscribeComponent(CEllipse::LButtonClick, "", "OnEllipse0LButtonClick"); node.SubscribeComponent(CEllipse::MouseMove, "", "OnEllipse0MouseMove"); node.SubscribeComponent(CEllipse::LButtonDown, "", "OnEllipse0LButtonDown"); node.SubscribeComponent(CEllipse::RButtonClick, "", "OnEllipse0RButtonClick"); node.SubscribeComponent(CEllipse::RButtonDown, "", "OnEllipse0RButtonDown"); node.SubscribeComponent(CEllipse::DoubleClick, "OnEllipse0DoubleClick", "OnEllipse0DoubleClick"); /* Занести новый узел в массив */ nodes[n_nodes] := node; n_nodes := n_nodes+1; /* Открыть узел, т.е. сделать его видимым */ node.open(); /* Сделать новый узел текущим */ curNode := node; curMarker.visible := true; curMarker.left := curNode.left; curMarker.top := curNode.top; end
Первый оператор создает новый объект класса CNode, устанавливая ему в качестве родителя Canvas и присваивая имя "Ellipse<n>", где <n> – это номер узла. Номера считаются при помощи переменной – члена класса CCanvas n_nodes, определенной следующим образом:
n_nodes int;
Позиция нового узла вычисляется с учетом текущего положения скроллинга Canvas так, чтобы созданный узел был всегда виден.
Последующие операторы вызывают метод SubscribeComponent нового объекта, чтобы подписать соответствующие методы класса CCanvas на события мыши нового узла.
На самом деле писать вручную эти вызовы вовсе не обязательно. Вместо этого можно создать в Canvas временный компонент Ellipse0, и при помощи визуальной среды LAB (Инспектора объектов) создать подписку на перечисленные события. Это действие, во-первых, сгенерирует в коде создания Ellipse0 последовательность нужных вызовов SubscribeComponent и, во-вторых, описание и заглушки для самих методов. После этого код подписки можно скопировать в метод NewNode, заменив 'Ellipse0' на 'node', а сам компонент Ellipse0 удалить, так как он больше не нужен (методы-подписчики при этом остаются).
После создания подписки на события метод NewNode заносит новый узел в массив узлов графа, делает его видимым и текущим. Здесь используются соответствующие переменные – члены класса CCanvas, которые мы описываем в его заголовке:
const maxNodes = 50; // максимальное количество узлов в графе curNode CNode; // ссылка на текущий узел nodes CNode array[maxNodes]; // массив узлов n_nodes int; // количество узлов
Визуально текущий узел выделяется при помощи прозрачного прямоугольника. Это обычный компонент CRectangle, который мы создали в Canvas при помощи среды LAB и назвали curMarker. CurMarker изначально невидим (свойство visible сброшено) и включается при создании первого узла.
Чтобы реализовать визуальное перемещение узла при помощи мыши, можно использовать следующую схему:
при нажатии левой кнопки мыши на узле (событие LButtonDown) сохраняем ссылку на перемещаемый узел и устанавливаем захват мыши этим узлом (захват обеспечивает, что все события от мыши будут приходить к данному компоненту, даже если курсор мыши выходит за его пределы);
в обработчике MouseMove изменяем координаты перемещаемого узла;
при отпускании левой кнопки (событие LButtonClick) освобождаем мышь.
В код эта схема воплощается следующим образом.
В методе OnEllipse0LButtonDown пишем:
dragNode := sender; // ссылка на перемещаемый узел curNode := sender; // он же становится текущим moved := false; // пока никуда не переместили // ставим маркер текущего узла curMarker.left := curNode.left; curMarker.top := curNode.top; dragNode.brushColor := "255 0 0 0"; // выделяем перемещаемый узел // сохраняем начальную позицию перемещения dragX := xPos; dragY := yPos; // захватываем мышь dragNode.setCapture();
Здесь используются дополнительные переменные – члены, которые необходимо определить в классе CCanvas:
dragNode CNode; // ссылка на перемещаемый узел dragX, dragY int; // координаты, которые имел узел до начала перемещения moved bool; // признак перемещения
В обработчике события MouseMove мы будем использовать координаты dragX, dragY, чтобы вычислить новые координаты узла исходя из того, что нам известно положение курсора мыши относительно левого верхнего узла.
dragNode.brushColor := "255 0 0 0";
устанавливает красный цвет заполнения узла (RGB 255 0 0).
Теперь напишем код в обработчик OnEllipse0MouseMove:
if dragNode <> null then dragNode.left := dragNode.left + xPos - dragX; dragNode.top := dragNode.top + yPos - dragY; moved := true; endif
(здесь xPos и yPos – параметры метода).
Осталось прописать обработку отпускания левой кнопки мыши в методе OnEllipse0LButtonClick:
if dragNode <> null then dragNode.brushColor := "128 128 128 0"; // вернуть обычный цвет dragNode.repaint(); if moved then moveNode(dragNode); // подвинуть линии связей endif dragNode.releaseCapture(); // отпустить мышь dragNode := null; curMarker.left := curNode.left; curMarker.top := curNode.top; endif
Метод moveNode предназначен для корректировки координат линий связей:
method moveNode(in node CNode) declare i int; link CNodeLink; code // Подкорректировать координаты исходящих ссылок for i := 0 while i < node.n_links_from with i:=i+1 link := node.links_from[i]; setLineCoords(link, link.first.left+10, link.first.top+10, link.sec.left+10, link.sec.top+10); endfor // Подкорректировать координаты входящих ссылок for i := 0 while i < node.n_links_to with i:=i+1 link := node.links_to[i]; setLineCoords(link, link.first.left+10, link.first.top+10, ink.sec.left+10, link.sec.top+10); endfor end
Цикл for в АТОЛЛ по функциональности аналогичен циклу for в языках C/C++ или Java
for(<выражение1>;<условие>;<выражение2>)
используется лишь другой синтаксис, более соответствующий "духу" языка АТОЛЛ:
for <выражение1> while <условие> with <выражение2>
Для установки координат линий мы используем простой метод:
method setLineCoords(in l CLine; in xx1, yy1, xx2, yy2 int) code l.x1 := xx1; l.y1 := yy1; l.x2 := xx2; l.y2 := yy2; end
Заметим, что если пользователь просто щелкает левой кнопкой на узле, то последовательно вызываются OnEllipse0LButtonDown и OnEllipse0LButtonClick. В итоге узел никуда не перемещается, но устанавливается в качестве текущего узла.
Для создания связи между узлами будем использовать следующий интерфейс:
нажимаем правую кнопку мыши на узле – источнике связи;
не отпуская правой кнопки, ведем линию по рабочей области (при этом рисуется линия от узла – источника до позиции курсора; когда курсор попадает в область какого-либо узла, он подсвечивается как потенциальный приемник связи);
при отпускании правой кнопки над узлом-приемником создается связь, при отпускании над свободной областью выходим из режима создания связи.
Основной принцип создания связи тот же, что и при перемещении узла.
Обработчик нажатия правой кнопки мыши имеет вид:
method OnEllipse0RButtonDown(in sender CComponent; in xPos int; in yPos int) result bool declare n CNode; code n := sender; // источник связи linkStart := n; // установить координаты "тянущейся" линии setLineCoords(line0, n.left+10, n.top+10, n.left+10, n.top+10); // включить эту линию line0.visible := true; // захватить мышь n.setCapture(); end
Здесь line0 – это компонент, который мы создаем в Canvas при помощи среды LAB. Он предназначен для изображения линии, "тянущейся" за курсором мыши. Изначально он невидим (свойство visible сброшено).
Дополнительная переменная – член класса CCanvas linkStart имеет тип CNode и предназначена для хранения ссылки на узел-источник создаваемой связи.
Теперь добавим соответствующий код в обработчик OnEllipse0MouseMove:
if linkStart <> null then // получить координаты курсора относительно Canvas linkStart.clientToScreen(xPos, yPos); screenToClient(xPos, yPos); // проверить, над каким компонентом находится курсор p := getChildFromPoint(xPos, yPos); // стандартный метод // если курсор находится над одним из узлов if p <> null and p <> linkStart and substr(p.name,1,7)="Ellipse" then // установить и подсветить потенциальный приемник связи linkEnd := p; p.brushColor := "255 0 0 0"; p.repaint(); else if linkEnd <> null then // сбросить потенциальный приемник связи linkEnd.brushColor := "128 128 128 0"; linkEnd.repaint(); linkEnd := null; endif // переместить линию, "следящую" за курсором setLineCoords(line0, linkStart.left + 10, linkStart.top + 10, this.xPos + xPos, this.yPos+yPos); line0.repaint(); linkStart.repaint(); endif endif
Здесь используется еще одна переменная-член класса CCanvas: ссылка на приемник связи linkEnd.
В вызове setLineCoords this.xPos и this.yPos используются для доступа к переменным-членам класса CCanvas (позиция сколлинга в Canvas), чтобы отличить их от параметров метода.
Теперь остается написать обработчик отпускания правой кнопки мыши:
method OnEllipse0RButtonClick(in sender CComponent; in xPos int; in yPos int) result bool declare link CNodeLink; code if linkStart <> null then if linkEnd <> null then // Создать и показать новую связь link := create CNodeLink(this, "Link"+tostring(total_links)) init(ZOrder := 2); link.open(); setLineCoords(link, linkStart.left+10, linkStart.top+10, linkEnd.left+10, linkEnd.top+10); // Сохранить в связи ссылки на узлы link.first := linkStart; link.sec := linkEnd; // Вернуть нормальный цвет приемнику связи linkEnd.BrushColor := "128 128 128 0"; // Добавить ссылки на связь в связуемые узлы linkStart.links_from[linkStart.n_links_from] := link; linkStart.n_links_from := linkStart.n_links_from+1; linkEnd.links_to[linkEnd.n_links_to] := link; linkEnd.n_links_to := linkEnd.n_links_to+1; // Увеличить общее количество связей total_links := total_links + 1; endif /* В любом случае при отпускании правой кнопки освободить мышь и скрыть line0 */ linkStart.releaseCapture(); linkStart := linkEnd := null; line0.visible := false; endif end
Переменная-член класса CCanvas total_links используется для подсчета общего количества связей в графе.
При создании связи значения всех свойств, кроме zOrder, берутся по умолчанию. zOrder необходимо устанавливать явно, чтобы линии связей оказывались под кругами, изображающими узлы графа.
Чтобы реализовать ввод дополнительных данных для узла (переменная txt класса CNode), создадим отдельную форму NodeParam. Разместим в этой форме компонент InfoMemo класса CMemo, который позволит показывать и редактировать многострочный текст, а также кнопки "ОК" и "Отмена". Форму NodeParam будем открывать по двойному щелчку на узле графа. Для этого в класс NodeParam включим метод
method EditNodeData(in node CNode) code mNode := node; infoMemo.text := node.text; doModal(); // открыть форму в модальном режиме end
Здесь mNode – это переменная-член класса NodeParam.
Вызов метода EditNodeData поместим в обработчик OnEllipse0DoubleClick класса CCanvas:
mainObj.NodeParam.EditNodeData(sender);
(стандартная переменная mainObj языка АТОЛЛ используется для доступа к объекту-приложению).
Чтобы после редактирования текст сохранялся в узле графа, в обработчик нажатия кнопки "ОК" необходимо вставить строки
mNode.txt := InfoMemo.text; ExitModal(1); // выйти из модального режима
Мы написали весь код, необходимый для создания или изменения узлов графа и связей, а также для ввода данных, ассоциированных с узлом.
Для корректной работы приложения осталось включить в конструктор класса CCanvas строки, инициализирующие переменные-счетчики:
n_nodes := 1; total_links := 0;
Теперь можно запустить приложение и проверить, как оно работает. Чтобы избежать дрожания узлов при перемещении, можно выключить выравнивание по сетке в Canvas (свойство gridSize установить в 0).
Пример работы нашей программы приведен на Рис. 21.
Чтобы напечатать граф, достаточно вызвать метод Print компонента CCanvas. Однако существует одна тонкость: компонент-отчет предназначен для печати данных из некоторого источника, и если он не привязан ни к одному источнику данных, то ничего печатать не будет. С другой стороны, если привязать его к источнику данных с одной строкой, то он ровно один раз напечатает тело отчета, то есть наш граф.
В нашем случае содержимое источника данных не важно, поэтому создадим компонент CMemory, укажем в его структуре одну колонку и добавим одну строку. После этого можно привязать Canvas к нашему источнику.
Теперь в обработчике нажатия кнопки печати, находящейся на панели инструментов, вставим вызов метода Print() для Canvas. Граф будет печататься.
Попробовав напечатать граф мы увидим, что результат засоряется маркером текущего узла и линией line0. Чтобы избежать этого, необходимо при помощи Мастера условий установить для них условие печати false.
В нашем примере мы нигде не вставляли проверок на переполнение массивов. Конечно, их необходимо сделать. Без этих проверок при попытке выхода за границу массива АТОЛЛ сгенерирует исключение ARRAYOVERFLOW. Программа может перехватить исключение и обработать его. Например, в методе NewNode мы могли бы написать следующий обработчик исключения:
exceptions when arrayoverflow then destroy node; message("Ошибка","Слишком много узлов!"); return false; end
В АТОЛЛ обработчики исключений пишутся в конце метода. Мы удаляем созданный узел, так как он не помещается в массив узлов. Наш обработчик исключения завершается возвратом из метода. Если бы мы не вставили оператор return, выполнение метода продолжилось бы с оператора, следующего за вызвавшим исключение. Обработчик исключения можно завершить также оператором resignal, который передает исключение в вызывающий метод (на уровень выше).
В нашем примере мы не стали описывать удаление узлов графа и связей. Реализовать эти операции достаточно просто: надо удалить объект (узел или связь) оператором destroy и вычистить соответствующие ссылки из массивов.
Перемещение узлов мы могли бы реализовать и используя механизм drag-and-drop. Однако объем кода вряд ли стал бы меньше: в любом случае необходимо прописывать реакцию на начало процесса перетаскивания (BeginDrag), перемещение (DragOver) и завершение процесса (Drop).
Наш пример можно развивать дальше. Можно реализовать сохранение и считывание графа. В качестве хранилища информации о графах можно использовать как базу данных, так и файлы (двоичные или текстовые). В АТОЛЛ есть все функции, необходимые для работы с БД и файлами.
В рассмотренном примере из управляющих операторов АТОЛЛ использовались операторы if, for и return. Кроме этих операторов, существуют операторы while (цикл "пока"), два варианта case (оператор выбора, когда варианты могут быть константами или выражениями) и goto (безусловный переход на метку).
LAB – молодая, развивающаяся система. Вряд ли кто-то может сегодня сказать какой она будет через несколько лет, но основные направления, по которым система будет развиваться можно назвать уже сегодня:
реализация средств работы с различными СУБД (с использованием ODBC, JDBC и, возможно, CALL-интерфейсов некоторых СУБД);
реализация средств создания WEB-приложений;
расширение набора используемых для разработки языков программирования: поддержка языков C++ и Java;
расширение библиотеки стандартных компонентов и встроенных функций, а также расширение возможностей подключения внешних компонентов (COM, CORBA);
расширение состава мастеров (мастера для генерации запросов, анализа схемы БД и генерации фрагментов кода для работы со связанными частями схемы БД).
Copyright © 1994-2016 ООО "К-Пресс"