My work on the screen form generator began with research into the file structure that defines the look and feel of projects created in the My Visual Database development environment. This time, we will learn the structure of the forms.xml file, which stores all the screen forms of the application, and at the same time we will learn how to read files in XML format.

Unfortunately, the TfxXMLDocument and TfxXMLItem components that are part of the available components for scripts have a number of functionality limitations that make working with them very difficult. In particular, TfxXMLDocument does not load documents in UNICODE encoding, but TfxXMLItem implies the internal representation in this particular encoding. Therefore, after several unsuccessful attempts to use these classes to read the forms.xml file, I decided to turn to OLE technology, in particular, use the MSXML2 offered by Microsoft.

The second serious problem on the way to the goal was the lack of full support for recursive procedure calls in FastScript. It seemed to me that the problem lies in the fact that all variables in FastScript are essentially global, there is no memory stack to store their values. But reading a tree structure implies exactly a recursive call, so I had to break my head over the organization of data storage, which is why the appearance of the recursive procedure came out not as elegant as expected.

Loading the form structure

To load the project’s form structure, use the ReadFormsXML() procedure, which creates an OLE object to work with the XML file, prepares the visual tree display component, and then calls the recursive Tree_AddNode() procedure.

procedure ReadFormsXML(AFileName: string;  ATree: TdbTreeView;);
// loading from xml to tree
var
  tmpDoc : Variant;
  tmpNode: Variant;
  tmpRow: integer;
begin
  if not FileExists( AFileName ) then
    ShowMessage('File not found '+AFileName)
  else
  begin
    tmpDoc := CreateOleObject('MSXML2.DOMDocument'); // create an automation object:
    tmpDoc.Load(AFileName); // load file
    tmpNode := tmpDoc.DocumentElement; // get first element
    ATree.BeginUpdate; // start data updating
    try
      Tree_Clear(ATree); // clean and prepare the tree
      // since MVDB does not support full recursion, we use global variables
      ArNode[0] := tmpNode; // put element into array
      Level := 0; // adjust level
      Tree := ATree; // tree
      Tree_AddNode( -1 ); // calling a recursive tree building function
      ATree.CollapseAll; // collapse all tree nodes
    finally
      ATree.EndUpdate; // completion of the tree update
    end;
  end;
end;
 
procedure Tree_AddNode( ARow:integer );
// recursive tree loading function
var
  tmpItemRow: integer;
  tmpPropRow: integer;
  tmpRow: integer;
  i: integer;
  tmpNode: Variant;
  tmpCount: integer;
  tmpNode2: Variant;
begin
  tmpNode := ArNode[ Level ]; // element
  if ARow = -1 then
    Tree.AddRow // tree root node
  else
    Tree.AddChildRow( ARow, crLast ); // child tree node
  tmpItemRow := Tree.LastAddedRow;
  Tree.Cells[0,tmpItemRow ] := tmpNode.NodeName; // name
  Tree.Cells[1,tmpItemRow ] := '';
  // attibutes
  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;
  // child elements
  tmpCount := tmpNode.ChildNodes.Length - 1;
  for i := 0 to tmpCount do
  begin
    // go one level deeper
    tmpNode := ArNode[ Level ];
    ArNode[ Level + 1] := tmpNode.ChildNodes.Item(i);
    inc(Level);
    Tree_AddNode( tmpItemRow); // recursive call
    dec(Level);
  end;
end;Code language: Delphi (delphi)

Loading the table structure

The structure of tables is the basis for solving the problem of automated construction of user interface forms. Since the structure is stored in UNICODE text .ini format, you need to use the TMemIniFile class to read such a file. To display the structure, I also decided to use a tree, in which the top-level elements will be tables, and the branches will be fields. The implementation of the loading algorithm is also made by two procedures: ReadIniFile() opens the file for reading, prepares the tree, and contains the main loop for adding information about tables. Tree_LoadTable() reads and adds information about the fields of a particular table.

procedure ReadIniFile( AFileName: string;  ATree: TdbTreeView;);
// reading the database structure from the initialization file
var
  tmpRow: integer;
  tmpTablesIni : TMemIniFile;
  tmpList : TStringList;
  tmpItemRow : integer;
  i: integer;
begin
  if not FileExists(AFileName) then
    ShowMessage('File not found '+AFileName)
  else
  begin
    // to work with this file use TMemIniFile - it works fine with Unicode
    tmpTablesIni := TMemIniFile.Create(AFileName);
    tmpList := TStringList.Create;
    try
      // read list of tables
      tmpTablesIni.ReadSections(tmpList);
      ATree.BeginUpdate; // tree update start
      Tree_Clear(ATree); // tree preparation
      for i := 0 to tmpList.Count - 1 do
      begin
        ATree.AddRow; // add table information
        tmpItemRow := ATree.LastAddedRow;
        ATree.Cells[0,tmpItemRow ] := tmpList.Strings[i];
        ATree.Cells[1,tmpItemRow ] := '';
        // adding information about table fields
        Tree_LoadTable( ATree, tmpTablesIni, tmpList.Strings[i], tmpItemRow );
      end;
      ATree.CollapseAll;
    finally
      ATree.EndUpdate; // end of tree update
      tmpList.Free;
      tmpTablesIni.Free;
    end;
  end;
end;
 
procedure Tree_LoadTable( ATree: TdbTreeView; ATablesIni : TMemIniFile; ATableName: string; ARow: integer );
// adding information about table fields
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 ); // add a child element
      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;Code language: Delphi (delphi)

Other procedures

I use the Tree_Clear() procedure to prepare the tree for data loading. It works with an instance of the TdbTreeView class: first, it clears it of rows and columns, and then creates the desired column configuration and sets up the visual representation of the tree.

procedure Tree_Clear(ATree:TdbTreeView);
// tree cleaning and preparation
begin
  ATree.ClearRows; // delete lines
  ATree.Columns.Clear; // delete columns
  // for a hitherto unknown reason it is necessary to escape adding columns
  try // add first column
    ATree.Columns.Add(TNXTreeColumn);
  except
  end;
  try // add a second column
    ATree.Columns.Add(TNxTextColumn);
  except
  end;
  // pseudographic elements
  TNXTreeColumn(ATree.Columns[0]).ShowLines := True;
  // setting up columns
  ATree.Columns[0].Color := clWhite;
  ATree.Columns[1].Color := clWhite;
  ATree.Columns[0].Header.Caption := 'Element';
  ATree.Columns[1].Header.Caption := 'Value';
  ATree.Columns[0].width := 300;
  ATree.Columns[1].width := 100;
end;Code language: Delphi (delphi)

To select a project, a button is used, in the click handler of which the main procedures for loading the form structure and table structure are called.

procedure Form1_btnSelectDir_OnClick (Sender: TObject; var Cancel: boolean);
// select a project and read its structure
var
  tmpDir: string;
  tmpFileName: string;
begin
  tmpDir := ExtractFileDir( Application.ExeName);
  if SelectDirectory('Select dir', '', tmpDir) then
  begin
    Form1.edtDir.Text := tmpDir;
    ReadIniFile( tmpDir + '\tables.ini', Form1.trvTables);
    ReadFormsXML( tmpDir + '\forms.xml', Form1.trvMain);
  end;
end;Code language: JavaScript (javascript)

Links

Leave a Reply

Your email address will not be published. Required fields are marked *