Continuation of the article ” Sophisticated simplicity”

Copagist is the most common disease of the source code of the program. Its genesis is as follows: it seems that it is enough to copy, having corrected a little, a certain section of the code several times and the result is ready! Why make it difficult to do if you can do it simply?

To begin with, a few words in defense of this methodology for achieving the goal. This can be done if you want to check some kind of concept, quickly make a prototype and show it to the final user. In this case, copy -paste is quite justified and even useful, as it saves the time of obtaining the first result.

However, if you are making a working project and want him (and you along with him) to live happily ever after, you need to get rid of copy-paste in the code. And that’s why.

Complexity of perception

A large number of lines makes it difficult to understand the program. Of course, the ability of people to memorize and perceive information is different (as are the sizes of monitors), but on average, for a programmer to work comfortably, it is necessary that the text of a subroutine (a logical unit of the source code) fit on 1-3 pages (I personally feel comfortable if it fits on one page).

Change Difficulty

If you change the functionality used in a copy-pasted section of code, you need to edit in several places. This usually leads to errors, since it is not always possible to remember where this copy-paste is located.

Practice

In the previous article, to initialize the application, the InitForm procedure code containing copy-paste was proposed (I will not copy it – because we decided to get rid of it 🙂 )

The easiest way (and the only one available in My Visual Database) to deal with copy-paste is to create a procedure or function where repeated commands need to be moved.

In our case, we will write a splitter creation function – Splitter_Create().

The separator (3) is usually used to resize two adjacent visual components (1,2) that are located on the parent component (4). The divider can be horizontal (as shown) or vertical. In this case, one of the separated components is aligned to the edge of the parent component, and the second is aligned (stretched) to the entire available area. The Align property is responsible for alignment, which is declared in the TControl class and is available for all its descendants, in particular, for all visual user interface components available in the MVDB component palette.

AlignDescription
alLeftPush to the left edge
alRigthPush to the right edge
alTopPush to the top edge
alBottomPush to the bottom edge
alClientStretch to the whole area
alNoneDo nothing (default)
Note. Unfortunately, in MVDB, the Align  property is not available from scripts for the TControl and TWinControl classes, so you will have to substitute in this place "crutch".

The Splitter_Create() function returns a reference to the created splitter.

function Splitter_Create( ASplitControl:TControl; AControl: TControl; AAlign:TAlign; AWidth:integer = 7 ): TSplitter;
// creating a splitter
// ASplitControl - control, the size of which will be changed by the splitter
// AControl - neighbor control, which usually occupies the rest of the space of the parent control
// AAlign - splitter alignment
// AWidth - Splitter width (height), if not indicated, then the constant splitter_def_size is used
begin
  // align the slide control
  // ASplitControl.Align := alLeft;
  // Unfortunately, in MVDN, the TContol class forgot to support the Align property, which is in the original Delphi class,
  // so you have to write more code:
  if ASplitControl is TdbPageControl then TdbPageControl(ASplitControl).Align := AAlign else
  if ASplitControl is TdbPanel then TdbPanel(ASplitControl).Align := AAlign;
  // create a splitter
  Result := TSplitter.Create( AControl.Owner );
  // add the splitter prefix to the name of the main control
  Result.Name := T_SPLITTER + ASplitControl.Name;
  Result.Parent := AControl.Parent;
  Result.Left := ASplitControl.Width + 1;
  Result.Width := AWidth;
  Result.Height := AWidth;
  Result.Align := AAlign;
  // align adjacent control
  // AControl.Align := alClient;
  // another inconvenience...
  if AControl is TdbPageControl then TdbPageControl(AControl).Align := alClient else
  if AControl is TdbPanel then TdbPanel(AControl).Align := alClient;
  // restore splitter position
  Splitter_LoadPosition(Result);
end;Code language: Delphi (delphi)

Now the initialization procedure looks more compact and clearer. But I had to add a constant for the prefix in the separator name and variables to store references to the separators.

const
  T_SPLITTER = 'spl'; // prefix for splitter
 
var
  // store links to splitters
  Spl_1:TSplitter;
  Spl_2:TSplitter;
  Spl_3:TSplitter;
  Spl_4:TSplitter;
 
procedure InitForm;
var
  tmpSplitter: TSplitter;
begin
  // remove main menu
  frmMain.Menu := nil;
  // reduce form flickering when redrawing
  frmMain.DoubleBuffered := True;
  // add splitters and alignment
  Spl_1 := Splitter_Create( frmMain.pgcClassView, frmMain.pgcClass, alLeft);   // classes
  Spl_2 := Splitter_Create( frmMain.pgcTypeView, frmMain.pgcType, alLeft); // types
  Spl_3 := Splitter_Create( frmMain.pgcFunctionView, frmMain.pgcFunction, alLeft); // functions
  frmMain.pgcVariableView.align := alClient;   // variables
  frmMain.pgcTaskView.align := alClient;   // tasks
  Spl_4 := Splitter_Create( frmMain.panMethod, frmMain.pgcMethodParamView, alTop); // methods
  //
  Form_ShowOnWinControl( frmClassTree, frmMain.tshClassTree );
  Form_ShowOnWinControl( frmClassList, frmMain.tshClassList );
  Form_ShowOnWinControl( frmTypeList, frmMain.tshTypeList);
  Form_ShowOnWinControl( frmFunctionTree, frmMain.tshFunctionTree );
  Form_ShowOnWinControl( frmFunctionList, frmMain.tshFunctionList );
  Form_ShowOnWinControl( frmVariableList, frmMain.tshVariableList );
  Form_ShowOnWinControl( frmTaskTree, frmMain.tshTaskTree );
  Form_ShowOnWinControl( frmTaskList, frmMain.tshTaskList );
  Form_ShowOnWinControl( frmProperty, frmMain.tshProperty );
  Form_ShowOnWinControl( frmMethod, frmMain.panMethod );
  Form_ShowOnWinControl( frmEvent, frmMain.tshEvent );
  Form_ShowOnWinControl( frmMethodParamList, frmMain.tshMethodParamList );
  Form_ShowOnWinControl( frmTypeConst, frmMain.tshTypeConst );
  Form_ShowOnWinControl( frmFunctionParam, frmMain.tshFunctionParam );
  Form_ShowOnWinControl( frmSearchResult, frmMain.tshSearchResult );
  Form_ShowOnWinControl( frmExampleView, frmMain.tshExample );
  //
  Tree_LoadCollapseList( frmFunctionTree.trvMain );
  Tree_LoadCollapseList( frmClassTree.trvMain );
  Tree_LoadCollapseList( frmTaskTree.trvMain );
end;Code language: Delphi (delphi)

We needed variables in order to organize the storage of the user-specified position of the separator. We will write this data to the settings.ini file, which already stores the column width settings in the tabular data view. To do this, let’s create a couple more procedures: Splitter_SavePosition() and Splitter_LoadPosition(). Please note that it is not the position of the separator that is saved, but the size of one of the components to which the separator is pressed. This is due to the operation of the component alignment mechanism: by changing the size of the component pressed to the edge, we automatically change the position of the separator and the size of the second component, which is stretched to the entire area (Align = alClient).

procedure Splitter_SavePosition( ASplitter:TSplitter );
// сохранение положения
var
  tmpPosition: integer;
  tmpControl: TControl;
  tmpForm: TAForm;
begin
  // it is necessary to record not the position of the splitter, but the size of the control whose size is changing
  CForm(ASplitter,tmpForm);
  // find master control
  FindC(tmpForm,DeleteClassName(ASplitter.Name),tmpControl);
  // depending on the type of alignment of the splitter, remember its position vertically or horizontally
  if (ASplitter.Align = alLeft) or (ASplitter.Align = alRight) then
    tmpPosition := tmpControl.Width
  else
    tmpPosition := tmpControl.Height;
  IniFile_Write('Splitters',TAForm(TComponent(ASplitter).Owner).Name+'.' +ASplitter.Name+'.Position', IntToStr(tmpPosition) );
end;
 
procedure Splitter_LoadPosition( ASplitter:TSplitter );
// restor of the splitters position
var
  tmpPosition: string;
  tmpControl: TControl;
  tmpForm: TAForm;
begin
  CForm(ASplitter,tmpForm);
  // find master control
  FindC(tmpForm,DeleteClassName(ASplitter.Name),tmpControl);
  //
  tmpPosition := IniFile_Read('Splitters',TAForm(TComponent(ASplitter).Owner).Name+'.' +ASplitter.Name+'.Position', '');
  if ValidInt(tmpPosition) then
  begin
    // depending on the type of alignment of the splitter, we restore its position vertically or horizontally
    if (ASplitter.Align = alLeft) or (ASplitter.Align = alRight) then
      tmpControl.Width := StrToInt(tmpPosition)
    else
      tmpControl.Height := StrToInt(tmpPosition);
  end;
end;Code language: Delphi (delphi)

Here, the attentive reader will notice that instead of directly writing or reading from the settings file, I call some functions IniFile_Write() and IniFile_Read(). This is due to the fact that I came up with the idea to save various parameters of other components in a similar way, and in order to avoid copy-paste … Right! You need to create a subroutine. Or two:

procedure IniFile_Write(ASection:string;AParamName:string;AValue:string);
// write string value
var
  tmpIniFile: TIniFile;
begin
  tmpIniFile := TIniFile.Create(Application.SettingsFile);
  tmpIniFile.WriteString(ASection, AParamName, AValue);
  tmpIniFile.Free;
end;
 
function IniFile_Read(ASection:string;AParamName:string;AValueDef:string=''):string;
// read string value
var
  tmpIniFile: TIniFile;
begin
  tmpIniFile := TIniFile.Create(Application.SettingsFile);
  Result := tmpIniFile.ReadString(ASection, AParamName, AValueDef);
  tmpIniFile.Free;
end;Code language: Delphi (delphi)

But here the right question arises: how to anticipate the occurrence of copy-paste and immediately create structured program code? This skill only comes with regular practice. The more code you write, the more clearly you will see the situations in which copy-paste occurs. But do not worry: you can get rid of copy-paste at any time, there would be a desire. And knowledge of the object model helps in this.

Object model is a collection of objects and enumerations that exist in the system, their properties, parameters and relationships. 

Actually, for the sake of studying and systematizing data according to the object model of My Visual Database, the application described in this article is being created. In the meantime, I’ll add a few more useful

Stubborn Tree

There lived a tree. Nice, pretty tree. He had a trunk and many branches with bright leaves. When the gardener asked the tree to roll up the branches, it obeyed him and turned into a small neat bush, which was a pleasure to look at. But as soon as a new leaf appeared on the tree, the tree immediately opened all its branches and it was no longer possible to see a new leaf in its lush crown. This made the gardener very upset and then he decided to write some procedures for the shrew tree.

procedure Tree_SeveCollapseList( ATree:TdbTreeView );
// save srttings
begin
  IniFile_Write('Trees',TAForm(TComponent(ATree).Owner).Name+'.' +ATree.Name+'.CollapseList',ATree.TagString);
end;
 
procedure Tree_LoadCollapseList( ATree:TdbTreeView );
// load settings
begin
  ATree.TagString := IniFile_Read('Trees',TAForm(TComponent(ATree).Owner).Name+'.' +ATree.Name+'.CollapseList', '');
end;
 
procedure Tree_RestoreCollapseList( ATree:TdbTreeView );
// restore from memory
var
  i:integer;
  j: integer;
  Nodes: array of string;
begin
  ATree.BeginUpdate;
  Nodes := SplitString(ATree.TagString,',');
  // use a list of record IDs to collapse nodes
  for j := 0 to ATree.RowCount-1 do
  begin
    for i := 0 to Length(Nodes) - 1 do
    begin
      if ATree.dbIndexToID(j) = StrToInt( Nodes[i] ) then
      begin
        ATree.Expanded[j] := False;
        break;
      end;
    end;
  end;
  ATree.EndUpdate;
end;
 
procedure Tree_SetCollapseList( ATree:TdbTreeView );
// store to memory
var
  i:integer;
begin
  // build a list from node record IDs that are collapsed
  ATree.TagString := '';
  for i:= 0 to ATree.RowCount-1 do
  begin
    if not ATree.Expanded[i] then
    begin
      if ATree.TagString <> '' then
        ATree.TagString :=  ATree.TagString + ',';
      ATree.TagString :=  ATree.TagString + IntToStr( ATree.dbIndexToID(i) );
    end;
  end;
end;Code language: Delphi (delphi)

The idea of ​​tree control is as follows: to write in the TagString property a list of record IDs for those nodes that have been collapsed. For this, the Tree_SetCollapseList() procedure is used, which can be called when the tree loses focus (the OnExit event). And at the moment when the tree updates its structure (the OnChange event), call a procedure that will collapse the necessary nodes. Remembering record IDs allows the algorithm to work correctly in case of changes in the tree structure (deleting and adding nodes). The structure is loaded (calling the Tree_LoadCollapseTree() procedure) when the program starts, in the InitForm() procedure, and saving (Tree_SaveCollapseTree()) is done when the program is closed (closing the main application form). The data on the position of the separators is also recorded there.

procedure frmMain_OnClose (Sender: TObject; Action: string);
begin
  Tree_SetCollapseList( frmFunctionTree.trvMain );
  Tree_SeveCollapseList( frmFunctionTree.trvMain );
  //
  Tree_SetCollapseList( frmClassTree.trvMain );
  Tree_SeveCollapseList( frmClassTree.trvMain );
  //
  Tree_SetCollapseList( frmTaskTree.trvMain );
  Tree_SeveCollapseList( frmTaskTree.trvMain );
  //
  Splitter_SavePosition( Spl_1 );
  Splitter_SavePosition( Spl_2 );
  Splitter_SavePosition( Spl_3 );
  Splitter_SavePosition( Spl_4 );
end;Code language: Delphi (delphi)

To be continued…

Leave a Reply

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