Моя работа над генератором экранных форм началась с исследований структуры файлов, которые определяют внешний вид и функциональность проектов, созданных в среде разработки My Visual Database. В этот раз мы узнаем структуру файла forms.xml, в котором хранятся все экранные формы приложения, а заодно научимся читать файлы в формате XML.

К сожалению, компоненты TfxXMLDocument и TfxXMLItem, которые входят в состав доступных компонентов для скриптов, имеют ряд ограничений функциональности, которая весьма затрудняет работу с ними. В частности, TfxXMLDocument не загружает документы в кодировке UNICODE, но при этом TfxXMLItem подразумевает внутреннее представление именно в этой кодировке. Поэтому после нескольких безуспешных попыток использовать данные классы для чтения файла forms.xml, я решил обратиться к технологии OLE, в частности использовать предлагаемый Microsoft MSXML2.
Второй серьёзной проблемой на пути к цели стало отсутствие полноценной поддержки рекурсивного вызова процедур в FastScript. Как мне показалось, проблема кроется в том, что все переменные в FastScript по сути глобальные, никакого стека памяти для хранения их значений нет. Но чтения древовидной структуры подразумевает именно рекурсивный вызов, поэтому пришлось поломать голову над организацией хранения данных, из-за чего внешний вид рекурсивной процедуры вышел не таким элегантным, как ожидалось.
Загрузка структуры форм
Для загрузки структуры форм проекта используется процедура ReadFormsXML(), в которой производится создание объекта OLE для работы с файлом XML, подготовка визуального компонента отображения дерева, а затем вызывается рекурсивная процедура Tree_AddNode().
procedure ReadFormsXML(AFileName: string; ATree: TdbTreeView;); // загрузка из XML в дерево var tmpDoc : Variant; tmpNode: Variant; tmpRow: integer; begin if not FileExists( AFileName ) then ShowMessage('Не найден файл '+AFileName) else begin tmpDoc := CreateOleObject('MSXML2.DOMDocument'); // создаём объект автоматизации: tmpDoc.Load(AFileName); // загружаем файл tmpNode := tmpDoc.DocumentElement; // получаем первый элемент ATree.BeginUpdate; // инициализация обновления дерева try Tree_Clear(ATree); // очистить и подготовить дерево // так как MVDB не поддерживает полноценную рекурсию, то используем глобальные переменные ArNode[0] := tmpNode; // элемент поместить в массив Level := 0; // настроить уровень Tree := ATree; // дерево Tree_AddNode( -1 ); // вызов рекурсивной функции построения дерева ATree.CollapseAll; // свернуть все узлы дерева finally ATree.EndUpdate; // завершение обновления дерева end; end; end; procedure Tree_AddNode( ARow:integer ); // рекурсивная функция загрузки дерева var tmpItemRow: integer; tmpPropRow: integer; tmpRow: integer; i: integer; tmpNode: Variant; tmpCount: integer; tmpNode2: Variant; begin tmpNode := ArNode[ Level ]; // элемент if ARow = -1 then Tree.AddRow // корневой узел дерева else Tree.AddChildRow( ARow, crLast ); // дочерний узел дерева tmpItemRow := Tree.LastAddedRow; Tree.Cells[0,tmpItemRow ] := tmpNode.NodeName; // название Tree.Cells[1,tmpItemRow ] := ''; // атрибуты for i := 0 to tmpNode.attributes.Length - 1 do begin Tree.AddChildRow( tmpItemRow, crLast ); tmpRow := Tree.LastAddedRow; Tree.Cells[0,tmpRow ] := tmpNode.attributes.Item(i).Name; Tree.Cells[1,tmpRow ] := tmpNode.attributes.Item(i).Value; end; // дочерние элементы tmpCount := tmpNode.ChildNodes.Length - 1; for i := 0 to tmpCount do begin // углубляемся на один уровень tmpNode := ArNode[ Level ]; ArNode[ Level + 1] := tmpNode.ChildNodes.Item(i); inc(Level); Tree_AddNode( tmpItemRow); // рекурсивный вызов dec(Level); end; end;
Загрузка структуры таблиц
Структура таблиц – это основа для решения задачи по автоматизированному построению форм пользовательского интерфейса. Так как структура хранится в текстовом формате .ini в кодировке UNICODE, то для чтения такого файла нужно использовать класс TMemIniFile. Для отображения структуры я решил также использовать дерево, в котором элементами верхнего уровня будут таблицы, а ветками – поля. Реализация алгоритма загрузки сделана также двумя процедурами: ReadIniFile() открывает файл для чтения, подготавливает дерево и содержит основной цикл добавления сведений о таблицах. Tree_LoadTable() производит чтение и добавление сведений о полях конкретной таблицы.
procedure ReadIniFile( AFileName: string; ATree: TdbTreeView;); // чтение структуры БД из файла инициализации var tmpRow: integer; tmpTablesIni : TMemIniFile; tmpList : TStringList; tmpItemRow : integer; i: integer; begin if not FileExists(AFileName) then ShowMessage('Не найден файл '+AFileName) else begin // для работы с этим файлом использовать TMemIniFile - он нормально работает с Unicode tmpTablesIni := TMemIniFile.Create(AFileName); tmpList := TStringList.Create; try // читаем список таблиц tmpTablesIni.ReadSections(tmpList); ATree.BeginUpdate; // начало обновления дерева Tree_Clear(ATree); // подготовка дерева for i := 0 to tmpList.Count - 1 do begin ATree.AddRow; // добавляем сведения о таблице tmpItemRow := ATree.LastAddedRow; ATree.Cells[0,tmpItemRow ] := tmpList.Strings[i]; ATree.Cells[1,tmpItemRow ] := ''; // добавляем сведения о полях таблицы Tree_LoadTable( ATree, tmpTablesIni, tmpList.Strings[i], tmpItemRow ); end; ATree.CollapseAll; finally ATree.EndUpdate; // конец обновления дерева tmpList.Free; tmpTablesIni.Free; end; end; end; procedure Tree_LoadTable( ATree: TdbTreeView; ATablesIni : TMemIniFile; ATableName: string; ARow: integer ); // добавление сведений о полях таблицы var tmpList : TStringList; tmpItemRow : integer; i: integer; begin tmpList := TStringList.Create; try ATablesIni.ReadSectionValues(ATableName, tmpList); for i := 0 to tmpList.Count - 1 do begin ATree.AddChildRow( ARow, crLast ); // добавляем дочерний элемент tmpItemRow := ATree.LastAddedRow; ATree.Cells[0,tmpItemRow ] := tmpList.Names[i]; ATree.Cells[1,tmpItemRow ] := tmpList.Values( tmpList.Names[i] ); end; finally tmpList.Free; end; end;
Другие процедуры
Для подготовки дерева к загрузке данных я использую процедуру Tree_Clear(). Она работает с экземпляром класса TdbTreeView: сначала очищает его от строк и столбцов, а затем создаёт нужную конфигурацию колонок и настраивает визуальное представление дерева.
procedure Tree_Clear(ATree:TdbTreeView); // очистка и подготовка дерева begin ATree.ClearRows; // удаляем строки ATree.Columns.Clear; // удаляем столбцы // по неизвестной до сих пор причине необходимо экранировать добавление колонок try // добавляем первый столбец ATree.Columns.Add(TNXTreeColumn); except end; try // добавляем второй столбец ATree.Columns.Add(TNxTextColumn); except end; // элементы псевдографики TNXTreeColumn(ATree.Columns[0]).ShowLines := True; // настраиваем колонки ATree.Columns[0].Color := clWhite; ATree.Columns[1].Color := clWhite; ATree.Columns[0].Header.Caption := 'Элемент'; ATree.Columns[1].Header.Caption := 'Значение'; ATree.Columns[0].width := 300; ATree.Columns[1].width := 100; end;
Для выбора проекта используется кнопка, в обработчике нажатия которой вызываются основные процедуры загрузки структуры форм и структуры таблиц.
procedure Form1_btnSelectDir_OnClick (Sender: TObject; var Cancel: boolean); // выбрать проект и прочитать его структуру var tmpDir: string; tmpFileName: string; begin tmpDir := ExtractFileDir( Application.ExeName); if SelectDirectory('Выберите папку', '', tmpDir) then begin Form1.edtDir.Text := tmpDir; ReadIniFile( tmpDir + '\tables.ini', Form1.trvTables); ReadFormsXML( tmpDir + '\forms.xml', Form1.trvMain); end; end;