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

Ссылки

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *