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 = nil
Code 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:
- No full style support
- 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 form | Dynamic form |
---|---|---|
Mechanism development costs | Zero. 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 form | Low | Low |
Costs for editing (updating) forms | Medium. 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 scripting | Average. Depends on the style of writing. Direct references to forms and components are allowed. | High. Access to forms or components only through procedures. |
Script Reliability | Average. 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 support | Complete | Partial. TdbStringGridEx component does not fully support application style |
Application compilation speed | Average. Depends on the number of forms | High. Does not depend on the number of forms |
Application launch speed | Average. Depends on the number of forms | High. 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
- “Component Explorer”
- “Butterfly effect” – article about ClassExplorer v.1.1
- ClassExplorer v.1.2 – program archive
- ClassExplorer v.1.2 – project files (only available to Visual Programming library subscribers)