Visual design tool allows you to create any form with the mouse, dragging the desired elements on the screen. It takes a few more minutes to edit the necessary parameters in the property editor. And if you need a lot of such forms and they are almost the same? Then, using CopyPaste technology, you can create a dozen forms and have nothing to worry about. Until you need to make any changes to the appearance or mechanics of these forms. And at this moment, developers begin to think about alternative solutions.

Dynamic UI

Often, a dynamic interface is understood as a mechanism for customizing the location of UI elements (buttons, floating toolbars, etc.), or interface behavior adapted to a specific user with his rights (hiding inaccessible elements: buttons or menu items), but I put a slightly different meaning into this definition.

A dynamic interface is a user interface that is created automatically, and the composition and functionality of its elements is determined by the database structure.

Dynamic interface is every developer’s dream: just design a database, and immediately a user interface is created to access it: tabular forms and editing forms. And it is worth editing the structure of the database, how the forms themselves are transformed: new input and display components are added or already unnecessary data display components are removed, links are rebuilt, sizes are aligned.

But this is a utopia: if the program is not trivial, then it is extremely difficult to create such a “reasonable” mechanism. Or the design of such an interface is so bad that the user only groans quietly, looking at the monotonous signs with small letters. But you always want to try.

ClassExplorer is an example of a program with a non-standard interface and a rather complex data structure. And this is quite suitable for an experiment on creating a dynamic interface. To make the experiment more convenient, I decided to add the ComponentExplorer – a script that allows you to see the contents of a project created in the My Visual Database development environment, as they say, from the inside.

Goals of the experiment

  • Explore the possibility of programmatically creating table view forms
  • Evaluating the complexity of programmatically creating edit forms
  • Comparison of visual and procedural programming technologies

Forms

As you know, the best experiment is a practical one: it is extremely difficult to come to the right conclusions by reasoning alone, so I decided to replace all forms with a tabular view with a script. In total, in the ClassExplorer project, I counted 15 tabular view forms, one of which is filled with an SQL query, and the rest were created only with the help of visual customization tools. Moreover, three of them display information in the form of a tree, and 6 depend on other forms – they use the button with the “Search” settings.

The structure of tabular forms is very simple: the form contains a table stretched over the entire client area.

For dependent forms, a button has been added with the configured “Search” function and an input field in which the ID of the parent record is written to filter data.

Scripts

To switch to dynamic table forms (DTF – Dynamic Table Form), I needed to write only two scripts: DTF_Create() to create the form and DTF_GetGrid() to access the data display component by the form name (for code optimization). It also required code optimization and the creation of a universal UserApp_btnUpdate_OnClick() handler for the onClick event for the btnUpdate button.

function DTF_Create(AName:string; ATable:string; AListFields:string; AListFieldsNames:string; ASort:string; AParentField:string; AFilter:string; AIsDetail:boolean = False):TForm;
// creating a dynamic form with a table view
// AName - form name; if it contains the _Tree suffix, then the tree form
// ATable - dbGeneralTable for tables and filter buttons; or dbForeignKey for tree
// AListFields - list of displayed fields, you can include calculated and fields from related tables; separator - comma; format: <table>.<field>
// AListFieldsNames - list of field labels
// ASort - dbSortField for tables; dbCustomOredrBy for trees
// AParentField - dbFieldParentID for trees and dbField for dependent tables
// AFilter - string for filtering data
// AIsDetail - a sign that the form is dependent
var
  tmpGrid: TdbStringGridEx;
  tmpButton: TdbButton;
  tmpEdit: TdbEdit;
begin
  // create a form
  Result := TForm.Create(Application);
  with Result do
  begin
    Name := T_DYNAMIC_TABLE_FORM + AName;
    Width := DTF_WIDTH_DEF;
    Height := DTF_HEIGHT_DEF;
    Position := poScreenCenter;
    Scaled := False;
  end;
  // create a table or tree
  if GetSuffix(AName) = SX_TREE then
  begin
    tmpGrid := TdbTreeView.Create(Result);
    tmpGrid.Name := T_TREE_VIEW+GRID_DEFAULT_NAME;
  end
  else
  begin
    tmpGrid := TdbStringGridEx.Create(Result);
    tmpGrid.Name := T_TABLE_GRID+GRID_DEFAULT_NAME;
  end;
  //
  with tmpGrid do
  begin
    parent := result;
    AppearanceOptions := aoAlphaBlendedSelection+aoBoldTextSelection;
    // font sizes and line heights
    Font Name := 'Segoe UI';
    Font.Size := 11;
    RowSize := 24;
    HeaderSize := 24;
    // Unfortunately, tables don't quite support styles when created programmatically
    // so for now the setting is for one style, then you need to come up with something
    EnableVisualStyles := True; // did not help...
    HeaderStyle := hsFlatBorders;
    //
    Color := $00CEDDD1; // clLime;
    Font.Color := $00294431;
// GridLinesColor := $00CEDDD1;
// SelectionColor := clBlack;
    InactiveSelectionColor := $00CEDDD1;
    HighlightedTextColor := $00294431;
    // use anchors so we can make a frame
    top := 0;
    Left := 0;
    Width := Result.ClientWidth;
    Height := Result.ClientHeight;
    Anchors := akTop+akBottom+akLeft+akRight;
    // to connect alternative handlers
    AssignEvents(tmpGrid);
    // start setting up data retrieval
    dbFilter := AFilter; // filtering
    dbListFields := AListFields; // list of displayed fields
    dbListFieldsNames := AListFieldsNames; // list of labels for fields
    if tmpGrid is TdbTreeView then
    begin
      TdbTreeView(tmpGrid).dbForeignKey := ATable; // main table
      TdbTreeView(tmpGrid).dbFieldParentID := AParentField;
      dbCustomOrderBy := ASort; // sort
    end
    else
    begin
      dbGeneralTable := ATable; // main table
      dbSortField := ASort; // sort
    end;
    // extract data so columns appear
    dbUpdate;
    // now event handlers
    if tmpGrid is TdbTreeView then
    begin // for tree
      dbOnCellClick := 'UserApp_UpdateDD';
      dbOnChange := 'Tree_RestoreCollapseList';
      dbOnClick := 'UserApp_SetActiveGrid';
      dbOnExit := 'Tree_SetCollapseList';
      dbOnKeyUp := 'UserApp_Grid_OnKeyUp';
      dbOnResize := 'Grid_OnResize';
    end
    else
    begin // for table
      OnCellClick := 'UserApp_UpdateDD';
      OnClick := 'UserApp_SetActiveGrid';
      dbOnKeyUp := 'UserApp_Grid_OnKeyUp';
      OnResize := 'Grid_OnResize';
    end;
  end;
  // add a frame
  Grid_AddFrame(tmpGrid);
  // now an option if the form is dependent and there is a main form
  if AIsDetail then
  begin
    // create a button
    tmpButton := TdbButton.Create(Result);
    AssignEvents(tmpButton);
    with tmpButton do
    begin
      parent := result;
      Name := 'btnUpdate';
      Visible := False;
      dbActionType := adbSearch;
      dbListControls := 'edtIDMaster';
      dbResultControl := 'tgrMain';
      dbOnClick := 'UserApp_btnUpdate_OnClick';
      //
      dbGeneralTable := ATable; // main table
      dbListFields := AListFields; // list of displayed fields
      dbListFieldsNames := AListFieldsNames; // list of labels
      dbSortField := ASort; // sort
    end;
    // create a field for ID
    tmpEdit := TdbEdit.Create(Result);
    with tmpEdit do
    begin
      parent := result;
      Name := 'edtIDMaster';
      Visible := False;
      dbFilter := '=';
      dbTable := ATable;
      dbField := AParentField;
    end;
    tmpButton.Click; // load data
  end;
  // load column width settings
  Grid_LoadColumnWidths(tmpGrid);
end;
 
function DTF_GetGrid(AFormName:string):TdbStringGridEx;
// access to the data display component by form name
var
  tmpForm: TForm;
begin
  tmpForm := App_GetFormByName(AFormName); // find the form
  if tmpForm = nilCode language: Delphi (delphi)

In the course of work, it turned out that the TdbStringGridEx class, when created using the Create() constructor, has several nuances in its work, namely:

  1. No full style support
  2. Column width settings not loading

These nuances can be bypassed. Added procedure Grid_LoadColumnWidths() to load width settings.

procedure Grid_LoadColumnWidths(AGrid: TdbStringGridEx;);
// restore the column width settings from the settings file
var
   tmpForm:TAForm;
   tmpName:string;
   tmpCol: integer;
   tmpIniFile: TIniFile;
begin
   tmpIniFile := TIniFile.Create(Application.SettingsFile);
   CForm(AGrid, tmpForm);
   for tmpCol := 0 to AGrid.Columns.Count - 1 do
   begin
     tmpName := tmpForm.name+'.'+AGrid.Name+'.'+IntToStr(tmpCol);
     AGrid.Columns[tmpCol].Width := tmpIniFile.ReadInteger('Grids',tmpName, 30);
   end;
   tmpIniFile.Free;
end;Code language: Delphi (delphi)

But with styles, there is no easy solution: you can set the colors of the table, but there is no way to determine these colors from the style settings. That is, for each style, you will have to write several constants that determine the color of the table, text, and cell selection in various modes.

The form initialization procedure looks rather strange. This clearly suggests a solution with tabular storage of form data. The only question is where to place such a table: in the database, in arrays, or in a separate .csv file

procedure UserApp_InitForm;
// form initialization
var
  tmpSplitter: TSplitter;
begin
  // remove main menu
  // frmMain.Menu := nil;
//  App_SetDoubleBuffer;
  App_AddFrame;
  // add splitters and alignment
  Spl_1 := Splitter_Create( frmMain.pgcClassView, frmMain.pgcClass, alLeft);   // классы
  Spl_2 := Splitter_Create( frmMain.pgcTypeView, frmMain.pgcType, alLeft); // типы
  Spl_3 := Splitter_Create( frmMain.pgcFunctionView, frmMain.pgcFunction, alLeft); // функции
  frmMain.pgcVariableView.align := alClient;   // переменные
  frmMain.pgcTaskView.align := alClient;   // задачи
  Spl_4 := Splitter_Create( frmMain.pgcMethodParamView, frmMain.panMethod, alBottom ); // методы
  Form_ShowOnWinControl( DTF_Create('ClassType_List',        'classType',    'classType.name',                                                                                          'Название',                 'classType.name',        '',            'isType = 0'), frmMain.tshClassList );
  Form_ShowOnWinControl( DTF_Create('Task_List',             'task',         'task.name',                                                                                               'Название',                 'task.name',             '',            'isGroup = 0'), frmMain.tshTaskList );
  Form_ShowOnWinControl( DTF_Create('ClassType_TypeList',    'classType',    'classType.name,classType.Description',                                                                    'Название,Описание',        'classType.name',        '',            'isType = 1'), frmMain.tshTypeList );
  Form_ShowOnWinControl( DTF_Create('FuncProc_FunctionList', 'funcProc',     'funcProc.name,funcProc.ResultType,funcProc.Description',                                                  'Название,Тип,Описание',    'funcProc.name',         '',            '( id_classType is NULL ) and (isGroup = 0)'), frmMain.tshFunctionList );
  Form_ShowOnWinControl( DTF_Create('Property_VariableList', 'property',     'property.name,property.TypeName,property.Description',                                                    'Название,Тип,Описание',    'property.name',         '',            '( id_classType is NULL )'), frmMain.tshVariableList );
  Form_ShowOnWinControl( DTF_Create('ClassType_Tree',        'classType',    'classType.name',                                                                                          'Название',                 'ParentID,name',         'parentID',    'isType = 0'), frmMain.tshClassTree );
  Form_ShowOnWinControl( DTF_Create('Task_Tree',             'task',         'task.name',                                                                                               'Название',                 'ParentID,name',         'parentID',    ''), frmMain.tshTaskTree );
  Form_ShowOnWinControl( DTF_Create('FuncProc_Tree',         'funcProc',     'funcProc.name,funcProc.ResultType,funcProc.description',                                                  'Название,Тип,Описание',    'ParentID,name',         'parentID',    'id_classType is NULL'), frmMain.tshFunctionTree );
  Form_ShowOnWinControl( DTF_Create('Property',              'property',     'property.name,property.TypeName,property.description',                                                    'Название,Тип,Описание',    'property.name',         'id_classType','',True), frmMain.tshProperty );
  Form_ShowOnWinControl( DTF_Create('FuncProc_Method',       'funcProc',     'FuncProc.name,FuncProc.ResultType,FuncProc.description',                                                  'Название,Тип,Описание',    'FuncProc.name',         'id_classType','',True), frmMain.panMethod );
  Form_ShowOnWinControl( DTF_Create('ClassEvent',            'classEvent',   'classEvent.name,classEvent.description',                                                                  'Название,Описание',        'classEvent.name',       'id_classType','',True), frmMain.tshEvent );
  Form_ShowOnWinControl( DTF_Create('FuncProcParam_Method',  'funcProcParam','funcProcParam.orderNum,funcProcParam.name,funcProcParam.description',                                     '№,Название,Описание',      'funcProcParam.orderNum','id_funcProc', '',True), frmMain.tshMethodParamList );
  Form_ShowOnWinControl( DTF_Create('FuncProcParam_Function','funcProcParam','funcProcParam.orderNum,funcProcParam.name,classType.name,funcProcParam.SubType,funcProcParam.description','№,Название,Тип,*,Описание','funcProcParam.orderNum','id_funcProc', '',True), frmMain.tshFunctionParam );
  Form_ShowOnWinControl( DTF_Create('TypeConst',             'typeConst',    'TypeConst.name,TypeConst.description',                                                                    'Название,Описание',        'TypeConst.name',        'id_classType','',True), frmMain.tshTypeConst );
  // static form
  Form_ShowOnWinControl( frmSearchResult, frmMain.tshSearchResult );
  Form_ShowOnWinControl( frmExampleView, frmMain.tshExample );
  // load state of tree nodes
  Tree_LoadCollapseList( TdbTreeView( DTF_GetGrid('dtfFuncProc_Tree') ) );
  Tree_LoadCollapseList( TdbTreeView( DTF_GetGrid('dtfClassType_Tree') ) );
  Tree_LoadCollapseList( TdbTreeView( DTF_GetGrid('dtfTask_Tree') ) );
  //
  UserApp_InitAboutForm;
end;Code language: PHP (php)

Conclusions

I decided to summarize the results of my research in a comparative table. The pros and cons are distributed, the choice of one or another technology for creating forms depends on the tasks that the developer has.

Aspect Static formDynamic form
Mechanism development costsZero. MVDB already has a visual form editor.High. It is required to design and develop a system for creating and managing dynamic forms.
Cost of creating a single formLowLow
Costs for editing (updating) formsMedium. Depends on the number of forms. Simultaneous editing of several form instances is required: setting their properties and/or rewriting handlers.Low. Do not depend on the number of forms. Point changes are made to the script.
Difficulty in scriptingAverage. Depends on the style of writing. Direct references to forms and components are allowed.High. Access to forms or components only through procedures.
Script ReliabilityAverage. Depends on the style of writing: if you use direct links to forms and components, then compile-time name control works.Low. There is no control of form and component names at compile time.
Style supportCompletePartial. TdbStringGridEx component does not fully support application style
Application compilation speedAverage. Depends on the number of formsHigh. Does not depend on the number of forms
Application launch speedAverage. Depends on the number of formsHigh. Does not depend on the number of forms

If there are few forms, their improvements are relatively rare, or the number of conventions in the interface is large, then it is preferable to use static forms.

If there are a lot of forms, the project is constantly being finalized and the work algorithms are strictly formalized, then I recommend dynamic forms.

What’s next?

Two years ago I made a good CRM/PM (Client Relation Management – client management system; Project Management – project management system), which I still use. But her look and the scripting style I used back then left a lot to be desired.

Therefore, my plans for the near future are to create a new project using all the progressive technologies available in My Visual Database (static rights system, static forms, visual styles, report generator, visualization of analytical graphs, etc.), and also those developments that I have accumulated during this time (elements of a dynamic interface, a dynamic system of rights, localization, graphic content management, etc.).

The ClassExplorer project will also evolve, but to a greater extent by filling the database with information about My Visual Database, the components and functions used, as well as adding new examples of solutions to practical problems.

Links

Leave a Reply

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