Продолжается подписка на наши издания! Вы не забыли подписаться?

LAB – отечественный инструмент быстрой разработки переносимых приложений

Алексей Эксаревский, 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. Структура базы данных простейшего тестового приложения

Пусть для хранения информации о владельцах и их автомобилях в БД имеются три таблицы, структура и связи которых приведены на рис 1.

Здесь символом <пк> помечен первичный ключ, а <вк> – внешний ключ. В таблице PEOPLE хранятся фамилии и фотографии людей, в таблице CARS – информация об их автомобилях, а таблица VEHICLE используется как справочник автомобилей (включает имя и внешний вид автомобиля).

Предположим, что эти таблицы существуют в базе данных СУБД ЛИНТЕР и их владельцем является пользователь SYSTEM.

Разработка приложения при помощи визуальной среды LAB

Создание проекта

Проект 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.

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

Рис. 3. Окно Инспектора объектов для выборки из БД

Для связи компонента Statement0 с соединением БД используется свойство connectName. Для установки этого свойства LAB активизирует окно, приведенное на рис. 4.

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

В LAB многие свойства компонентов устанавливаются как ссылка на другой компонент определенного класса. Выбор компонента всегда осуществляется при помощи приведенного на рис. 4 окна. Причем компонент может ссылаться на любой другой компонент, даже если они находятся в разных формах и один из них еще не создан. Достигается это за счет того, что свойство-ссылка хранит путь к компоненту, который формируется способом, похожим на формирование пути к файлу в файловой системе. В качестве имен каталогов здесь выступают имена контейнерных компонентов, а в качестве имен файлов – имена дочерних компонентов. Имя компонента передается как параметр его конструктору.

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

Рис. 4. Окно установления связи с соединением с БД

Вернемся к нашему источнику данных. После того, как выбрано соединение, необходимо ввести select-запрос на выборку данных. В общем случае запрос вводится в свойство SQLQuery. Однако в нашем случае необходим простой запрос на выборку всех данных из таблицы PEOPLE. Для этого достаточно просто ввести (или выбрать) имя таблицы в свойство MainTable. В этом случае LAB генерирует запрос типа select * from PEOPLE.

По умолчанию все изменения свойств, произведенные в Инспекторе объектов, сохраняются по специальной кнопке «Применить». При нажатии кнопки «Отмена» или при смене активного компонента все сделанные изменения свойств аннулируются. При помощи меню «Опции» пользователь может включить режим автоматического применения свойств после изменения в Инспекторе объектов.

Свойство Structure источника данных позволяет настроить его структуру, то есть имена и типы колонок, а так же связь с соответствующими полями запроса. Структура задается при помощи диалогового окна, приведенного на рис. 5.

Рис. 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 можно настроить визуально. Каждая колонка имеет дополнительные свойства, которые можно изменить по двойному щелчку мышью на заголовке колонки.

Рис. 6. Форма с компонентами, показывающими данные из таблицы PEOPLE

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

Связующие компоненты можно использовать для связи произвольных двух источников данных, в том числе уже участвующих в других связях. При этом образуется сложная разветвленная структура связанных источников данных, в общем случае имеющая вид графа. Причем все связи отрабатываются компонентами автоматически, без дополнительного программирования. Имеется механизм защиты от зацикливания связей.

Рис. 7. Окно установки полей связи двух источников данных

В нашей форме марки автомобилей показываются правильно, однако, не хватает механизма для выбора автомобиля из справочной таблицы 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).

Рис. 8. Привязка колонки таблицы к словарю

После того, как колонка привязана к словарю, в ячейках для ввода марки автомобиля появляется кнопка, по нажатию которой появляется список справочных значений (согласно запросу, указанному в словаре). Вид данного окна приведен на рис. 9.

В стандартном окне выбора словарного значения поддерживается функция контекстного поиска значения, если эти значения отсортированы. Сортировка выполняется автоматически по соответствующему полю, если у словарной колонки установлен признак автосортировки, поэтому мы не указали order by в запросе на выборку.

Рис. 9. Стандартное окно для выбора словарного значения

Кроме колонки таблицы, к словарю можно также привязать выпадающий список (combobox).

LAB позволяет вместо стандартного окна выбора словарного значения использовать свое собственное. Для этого создается отдельная форма, ссылка на которую указывается в свойстве RefForm словаря. Форма получает доступ к словарю через специальный компонент – формальный параметр, так что одну и ту же форму выбора можно использовать для разных словарей. В нашем примере мы могли бы разработать форму, позволяющую выбирать автомобили из иерархического списка, отображая дополнительно их изображения.

Ввод информации в базу данных

Созданная в предыдущем пункте форма пригодна не только для просмотра информации из базы данных, но и для ввода информации, так как источники данных способны отображать на таблицу, которая указана в свойстве mainTable, все изменения. Например, можно изменять данные, которые показываются в табличных компонентах Grid0 и Grid1, прямо в этих компонентах. Как уже было сказано, изменения кэшируются компонентами. Для того чтобы данные были реально внесены в БД, необходимо вызвать соответствующий метод источника данных. Еще один метод источника данных позволяет наоборот, сбросить информацию в кэше и обновить источник данных в соответствии с содержимым базы данных.

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

Таким образом, в нашей форме не хватает только механизма вызова перечисленных методов. В простейшем случае этот механизм обеспечивается компонентом-навигатором CDBNavigator (). Этот компонент имеет фиксированное число кнопок, предназначенных для выполнения перечисленных функций, а так же для навигации по записям источника данных. Часть кнопок может быть выключена при помощи специальных свойств.

Добавим компонент-навигатор в нижнюю часть нашей формы. Чтобы он работал, его необходимо связать с источником данных при помощи свойства dataSource, так как это делалось ранее для визуальных компонентов. Связав навигатор со SmartStatement0, мы получаем возможность внесения изменений в таблицу автомобилей. Причем при добавлении новых записей связующий компонент StmtLink0 обеспечивает занесение в поле "CARS.IDP" идентификатора текущего владельца, поэтому новая запись оказывается связанной именно с данным владельцем, как и требовалось. Отметим еще раз, что для фиксации изменений в БД необходимо нажать кнопку навигатора для сохранения изменений.

Для того чтобы реализовать возможность изменения данных в таблице PEOPLE, можно добавить в форму еще один навигатор, связав его с источником данных Statement0.

Создание меню

Меню представляет собой совокупность иерархически организованных компонентов CMenuItem, расположенных внутри функционального компонента CMenu. Так как эта структура достаточно громоздка, для создания меню используется мастер меню, позволяющий визуально создавать и редактировать меню (рис. 10).

Рис. 10. Окно Мастера меню

Для того чтобы меню отобразилось в форме, необходимо связать форму с данным меню. Для этого используется свойство Menu формы, которое является ссылкой на меню. Одно и то же меню может использоваться в нескольких формах. При установке ссылки на меню оно автоматически отображается в форме.

Окончательный вид формы с меню приведен на рис. 11.

Рис. 11. Окончательный вид главной формы приложения

Обработка событий

Чтобы меню заработало, необходимо создать соответствующие обработчики события выбора его пунктов. Допустим, мы хотим, чтобы по пункту «Выход» осуществлялся выход из приложения.

Для этого можно щелкнуть мышью по нужному пункту меню в форме. В результате появится окно подписки на событие (рис. 12).

Рис. 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. Пример окна редактора в процессе отладки

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

Рис. 14. Диалог формирования структуры отчета

В наш отчет мы включаем заголовок и конец страницы, а также заголовок и конец группы по полю PEOPLE.NAME. В результате в компоненте-отчете появляются соответствующие области, в которых мы и разрабатываем форму отчета (рис. 15). Размер любой области можно изменить визуально.

Рис. 15. Вид формы отчета с несколькими областями

Форма отчета разрабатывается, в основном, при помощи компонентов двух видов: графических примитивов (прямоугольник и линия) для изображения рамок, линий и визуальных элементов, привязанных к источнику данных.

Графические примитивы можно проводить через несколько областей сразу, что облегчает разработку таблиц.

Компоненты, привязанные к источнику данных, удобно создавать при помощи Мастера связи с источником данных (рисунок 16).

Мастер позволяет выбрать источник данных, его поле, зафиксировать кнопку с изображением соответствующего компонента, и создать этот компонент в форме. Это быстрее, чем вначале создать визуальный компонент, а уже затем привязать его к источнику данных при помощи свойств dataSource и dataField. Кроме того, мастер позволяет наглядно просматривать, с каким источником связан активный визуальный компонент, и, при необходимости, изменять эту связь.

Для нашего отчета выполним следующее:

Рис. 16. Мастер связи с источником данных

В результате форма получит вид, приведенный на рис. 17.

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

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

Рис. 18. Окно предварительного просмотра результатов отчета

Добавим в меню «Файл» пункт «Отчет» (вызвав Мастер меню по двойному щелчку на компоненте-меню) и подпишем на него нашу форму отчета Form1. Вставим в тело метода-обработчика вызов метода Print(), который и обеспечивает генерацию отчета.

Теперь можно запустить наше приложение.

Рис. 19. Просмотр результата экспорта отчета в HTML

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

АТОЛЛ поддерживает следующие типы данных:

АТОЛЛ позволяет присваивать переменной любого типа значение 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).

Рис. 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 сброшено) и включается при создании первого узла.

Перемещение узла

Чтобы реализовать визуальное перемещение узла при помощи мыши, можно использовать следующую схему:

В код эта схема воплощается следующим образом.

В методе 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. Граф будет печататься.

Рис. 21. Пример работы редактора графов

Попробовав напечатать граф мы увидим, что результат засоряется маркером текущего узла и линией 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

LAB – молодая, развивающаяся система. Вряд ли кто-то может сегодня сказать какой она будет через несколько лет, но основные направления, по которым система будет развиваться можно назвать уже сегодня:


Copyright © 1994-2016 ООО "К-Пресс"