Last Updated:

Directory Form Creation Technology [borland C++]

For beginners, and not only, programmers, it is often a big problem to create the same type of forms for working with reference books and similar tasks. At least a large number of questions both on the forums and in person, testifies to this. Therefore, at the request of one of my friends, I decided to devote a separate article to the issue of organizing work with forms - and in particular the organization of work with the same type of forms. After all, in large projects, it is not uncommon to have a huge number of different directories - such as job directories, department directories, directories of payment types and their numbers. Moreover, in the process of operating the program, an additional increase in the number of directories is not uncommon - which in itself delivers. So how do you deal with that? I will try to give the answer to this in my article. I would like to draw your attention to the fact that these recommendations are based on personal practical experience in developing large software systems. The article will cover two main topics

  1. Using the Model-View-Controller (MVC) design pattern in your work
  2. Dynamically create forms, as I do.

These two topics are almost inseparable from each other - for one simple reason - if you do not use MVC, sooner or later, in a relatively large project, you will begin to write terrible "pasta" code in which you will get confused yourself. To avoid this, use good programming patterns.

All of the examples in this article are based on Borland's development tools, Borland Builder C++, but can essentially be used on any system, regardless of programming language. BCB is only used here because it is written by the person who asked to write this article.

And so MVC. What is it, what is it eaten with and why exactly do you need to use this template? It's that simple. the basic principle of using MVC is to separate the code - if you separate the code that receives data from the database, from the code that processes this data and separately generate the code that the result of this processing shows - congratulations - you use the MVC. Most successful software tools are designed in such a way that following this pattern in them is quite simple, easy and intuitive. But BCB and Delphi are not among such systems (for which, I think, their creators will burn in Hell - in a specially created 10 circle for them), because in them it is extremely simple and easy to write "pasta" bullcode - when all the data acquisition, processing and output is shoved into one single class of a single form, on which the programmer threw grief under a hundred controls - and all this, if I may say so, a code of several thousand lines is compiled into an executable file and proudly called a "product".

In principle, there is nothing terrible in this - unless of course you have to subsequently maintain and maintain all this. Because, in the process of servicing and supporting a working program, one way or another, you have to expand it, refine and... and that's where the Armaged comes in. Because, after a few months, it is difficult to remember why that function was written - which is called 20 times in different places, and why in some places completely different code is called in place of this function ... and so on. Those who had to support the "products" of the vital activity of such "programmers" will understand me.

How to put all this into practice? Let's design a small module of the system — suppose our module implements the following functionality:

  1. Display and display records of various directories.
  2. Add, edit, and delete selected records.
  3. Provide a single interface for retrieving directory entries (lists, trees, and so on) anywhere in the application.

For starters, enough is enough. If you use the standard approach, then usually a form is created, the necessary controls and components are stuffed into it - then another form (and often more than one) is created for editing and adding. Data retrieval from directories is performed independently at each point in the application.

How do you do that from an MVC perspective? You must design four classes—the controller class, the data access class, the primary form class, and the editing add form class. Since reference is English Reference, I personally call the classes as follows:

  1. TRefernceController is a controller class that inherits from a common controller class (if desired);
  2. TmfMain is a basic form class that inherits from TForm (BCB)
  3. TfmAddEdit is a helper form class that inherits from TForm (BCB);
  4. TdmMain is a data module that inherits from TDataModule (BCB).

Naturally, class inheritance depends on your particular application - I took the simplest option, so as not to go too deep and not to get confused. The name of the modules also depends on which naming system you have adopted. For example, TmfMain - I have a name for any basic form of the module, but that's the details and I'll leave that to your attention.

An overview of the controller class might look something like this:

 
Code:
 
#ifndef loginH
#define loginH
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ComCtrls.hpp>
#include <Buttons.hpp>
#include <ExtCtrls.hpp>
#include "main.h" //, connect the header files of the forms
#include "addedit.h"

class TReferenceController :public TGlobalController, if necessary, inherit from our common class
{
TfmMain *fmMain;
TfmMain *dmMain;
... other public private ads
:
__fastcall TRefernceController(TConnection* db);
void __fastcall getList(TStrings *list, const int index);
... Other public methods
}
#endif
 

As you can see, there is nothing particularly complicated here. The specific implementation of the controller depends on the tasks of the application, but the essence of this does not change - the task of the class is to provide all work with objects, hiding all the details and subtleties from the user-application. The main point is that I pass a pointer to the existing connection in the application to the class constructor, which would be something more than initializing the database connection module.

The data module class, in turn, provides a connection to the database, obtaining the necessary data, creating, editing and deleting data in tables.

His ad might look something like this:

 
Code:
 
#ifndef maindmH
#define maindmH
---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ADODB.hpp>
#include <DB.hpp
> #include <Provider.hpp>
#include <DBClient.hpp>

---------------------------------------------------------------------------
class TdmMain : public TDataModule
{
__published: IDE-managed Components
TADOConnection *adoConnect;
TDataSource *dsListOrder;
TADOQuery *adoListOrder;
private: // User declarations
int UserId;
Other private methods and public properties
: User declarations
__fastcall TdmMain(TComponent* Owner, const bool visibleForm = true);
__fastcall TdmMain::TdmMain(TComponent* Owner, TConnection *db);
void __fastcall getListReference(TStrings *list);
Other public methods

}
//---------------------------------------------------------------------------
extern PACKAGE TdmMain *dmMain;
//---------------------------------------------------------------------------
#endif
 

There is nothing special about the description of the forms, so I think the code is superfluous here.

The general algorithm is as follows:

  1. The application initializes the module;
  2. Creates an object of the TReferenceController class by calling its constructor and passing it a pointer to an open connection to the database;
  3. A data module object is created as constructor parameters;
  4. If the form creation flag is set, we create the form.

Sample controller designer code:

 
Code:
 
__fastcall TReferenceController::TReferenceController(TConnection *db, const bool visibleform):dmMain(NULL,db)
{
if necessary - create a form, fill it out and show it modally
if(visibleForm)
{
fmMain = new TfmMain(NULL);
dmMain->getListTab(fmMain->PageContorl,0); download the tabs of references and set the fmMain->ShowModal index
();
  }
}
 

I think the basic idea is quite clear. Naturally, the use of MVC complicates life a little at the first stage - but it greatly simplifies it in the future.

If, however, the idea of using a controller still seems too complicated to you - you can use a simplified version - two forms and a data module. In this case, receiving, processing and writing data is implemented in a data module (dmMain) and, accordingly, all components for working with the database are located there. Forms, in turn, implement the display and processing of data entry.

And here the main problem for beginners arises with how to create a form dynamically and pass the necessary data to it. Here's how: Suppose that our database contains reference tables of the following form:

 
Code:
 
CREATE TABLE [dbo]. [Spr_Podr] (
[Kod] [int] IDENTITY (1, 1) NOT NULL , autoincremental
[KodMR] [int] NULL, code of work
[MR] [nvarchar] (150 ) COLLATE Cyrillic_General_CI_AS NULL, name of place of work
[prMR] [nvarchar] (250) COLLATE Cyrillic_General_CI_AS NULL, name of the place of work for the formation of
the order [arh] [int] NOT NULL signature entry in the archive 0 or 1
) ON [PRIMARY]

CREATE TABLE [dbo]. [Spr_Slugb] (
[Kod] [int] IDENTITY (1, 1) NOT NULL , auto-incremental
[KodPodr] [int] NULL, service (department)
code [Podr] [ nvarchar] (250) COLLATE Cyrillic_General_CI_AS NULL , name of service (department)
[arh] [int] NOT NULL signature entry in archive 0 or 1
) ON [PRIMARY]

CREATE TABLE [ dbo]. [Spr_PodPodr] (
[Kod] [int] IDENTITY (1, 1) NOT NULL , autoincrement [
KodPPodr] [int] NULL, subsection
code [PodPodr ] [nvarchar] (255) COLLATE Cyrillic_General_CI_AS NULL , subsection
name [arh] [int] NOT NULL attribute entry in archive 0 or 1
) ON [PRIMARY]

CREATE TABLE [dbo]. [SprDolg] (
[id] [int] IDENTITY (1, 1) NOT NULL , autoincrement [
Kod] [int] NULL , code position
[Dolg] [nvarchar] (150) COLLATE Cyrillic_General_CI_AS NULL , job
title [Dolg_K] [nvarchar] (150) COLLATE Cyrillic_General_CI_AS NULL , job title for card
[K_go] [nvarchar] (150) COLLATE Cyrillic_General_CI_AS NULL, job title - who
[K_mu] [nvarchar] (150) COLLATE Cyrillic_General_CI_AS NULL, job title - to whom
[K_m] [nvarchar] (150) COLLATE Cyrillic_General_CI_AS NULL job title - kim
[arh] [int] NOT NULL signature entry in the archive 0 or 1
) ON [ PRIMARY
]
 
 

as you can see, in general, the directories are similar - they differ only in the number of text fields that are written and read in the database, i.e. three fields in all directories are of the same type - these are the fields id (Kod), Kod*, arh - the first two are numeric values, the last - can contain either 0 or 1. The id field is autoincrement - i.e. it does not need an input field, it can only be displayed. Although there are tasks when you need to be able to install it manually, then you need to implement a separate operation - but in this case there is no such need. The User must be able to set the Kod field with their hands - i.e. for it we provide a separate input text field. For the arh field, a checkbox is enough for us - since it can only have two values. And at a minimum, every reference should have at least one text field. The structure is of course so-so, there is nothing to say here. The person who designed it is a complicated person, to put it mildly. :) But here I will not dwell on the principles of database design, I will only say that if you implement such a thing, try to observe uniformity. If all directories have the same type of fields, use the same names for them. The key field should not be in one of the tables in one case Kod and in the second id. Don't complicate your own lives.

To work properly with directories - I would recommend adding two additional tables to the database, the task of which is to store metadata for display. In the first of them, store the directory ID, the name of the directory table, possibly the name of the directory to display (if the system is monolingual, or the name for the default language) and other parameters that you may need, and in the second table, respectively, the id of the record, the identifier of the directory, the name of the field and the name of the field to display. Well, and other parameters that will help you customize the display of each of the fields.Tables of course should be related by the identifier of the directory. If you feel in your heart that natural keys are better than artificial ones, you can use the name of the reference table as a key. It's on the amateur.

In this case, for each directory, the task is simplified as much as possible. Once the tables are created and populated, the next step is to create the main form. Remember, we talked about the controller above – so the easiest way to do it is through it. If this model seems too complicated to you, then you can do without it. First of all, we determine how our data will be loaded. Since there can be one or more directories, we use a TPageControl. Place it on the form, call pcMain, configure the necessary parameters. In principle, there is nothing particularly complicated in this - so I will illustrate everything in the future with code.

The code of the main form:

 
Code:
 
.h
#ifndef mainfmH
#define mainfmH
---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ComCtrls.hpp>
#include "maindm.h"
#include <DBGrids.hpp>
#include <Grids.hpp>
---------------------------------------------------------------------------
class TfmMain : public TForm
{
__published: IDE-managed Components
TPageControl *pgMain;
void __fastcall pgMainChange(TObject *Sender);
void __fastcall FormDblClick(TObject *Sender);
private: // User declarations
TdmMain *dmMain;
TDBGrid * __fastcall getCurrentGrid();
public: // User declarations
__fastcall TfmMain(TComponent* Owner);
__fastcall TfmMain(TComponent* Owner, TADOConnection *db);
};
//---------------------------------------------------------------------------
extern PACKAGE TfmMain *fmMain;

--------------------------------------------------------------------------- #endif
.cpp
---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include "mainfm.h"
#include "addedit.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TfmMain *fmMain;
//---------------------------------------------------------------------------
__fastcall TfmMain::TfmMain(TComponent* Owner)
: TForm(Owner),dmMain(Owner)
{
here you write the code in which you initialize the connection to the database. If the database is already connected - another constructor
is used }
__fastcall TfmMain::TfmMain(TComponent* Owner, TADOConnection *db)
: TForm(Owner),dmMain(Owner,db)
{
dmMain->loadPages(this->pgMain);
dmMain->loadReference(getCurrentGrid());
}

//---------------------------------------------------------------------------
TDBGrid * __fastcall TfmMain::getCurrentGrid()
{
if(pgMain->ActivePageIndex == - 1)return;
TDBGrid* db = NULL;
for(int i = 0; i < this->ComponentCount; ++i){
if(this->Components->ClassName()! = "TDBGrid")continue;
db = dynamic_cast<"TDBGrid">(this->Components);
if(db->Parent == pgMain->ActivePage) break;

}
return db;
}
void __fastcall TfmMain::pgMainChange(TObject *Sender)
{
dmMain->loadReference(getCurrentGrid());
}
//---------------------------------------------------------------------------
void __fastcall TfmMain::FormDblClick(TObject *Sender)
{
TStringList *ls;
TfmAddEdit *fm;
try{
fm = new TfmAddEdit(NULL);
get the redirect key
fm->id = dmMain->adoQuery->Fields-<b1308&gt;&gt;</b1308>FieldByNumber(0)->AsInteger;
here we get the parameters of our fields from the add-table to the list
ls = new TStringList;
dmMain->loadReferenceFields(ls);
here we process our list - how to create objects and initialize their parameters
is shown above. do not forget to check the height of the shape and the height of the objects
...
}
__finally{
if(fm)delete fm;
if(ls)delete ls;
}

}
---------------------------------------------------------------------------
Data module
code:
.h
---------------------------------------------------------------------------

#ifndef maindmH
#define maindmH
---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ADODB.hpp>
#include <DB.hpp>
#include <DBGrids.hpp>
#include <Grids.hpp>

---------------------------------------------------------------------------
class TdmMain : public TDataModule
{
__published: IDE-managed Components
TADOConnection *adoConnect;
TADOQuery *adoQuery;
TDataSource *dsQuery;
TADOQuery *adoExecute;
private: // User declarations
public: // User declarations
__fastcall TdmMain(TComponent* Owner);
__fastcall TdmMain(TComponent *Owner, TADOConnection *db);
void __fastcall loadPages(TPageControl* pg,const int index = 0);
void __fastcall loadReference(TDBGrid *gb);
void __fastcall loadReferenceFields(TStrings *ls);
};
//---------------------------------------------------------------------------
extern PACKAGE TdmMain *dmMain;

--------------------------------------------------------------------------- #endif
---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include "maindm.h"
---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
#include "mainfm.h"
TdmMain *dmMain;
//---------------------------------------------------------------------------
__fastcall TdmMain::TdmMain(TComponent* Owner)
: TDataModule(Owner)
{

}
void __fastcall TdmMain ::loadPages(TPageControl *pg, const int index)
{
TTabSheet *tab;
adoExecute->SQL->LoadFromFile("loadreferense.sql");
adoExecute->Active = true;
while(! adoExecute->Eof){
tab = new TTabSheet(pg);
tab->Name = adoExecute->FieldByName("tablename")->AsString;
also configure other parameters if necessary
TDBGrid *grid = new TDBGrid(tab);
grid->Name = "gd"+tab->Name;
grid->Parent = tab;
grid->Tag = adoExecute->FieldByName("referenceid")->AsInteger;
grid->Align = alClient;
grid-> OnDblClick = fmMain->OnDblClick; install the
double-click handler adoExecute->Next();
}
if(pg->PageCount > index) pg->ActivePageIndex = index;
else pg->ActivePageIndex = 0;
}
void __fastcall TdmMain::loadReference(TDBGrid *gb)
{
if(! gb)return;
adoQuery->Active = false;
adoQuery->LoadFromFile(gb->Name+".sql");
adoQuery->Active = true;
gb->DataSource = dsQuery
;
}
//---------------------------------------------------------------------------

As you can see in the code, almost all components are created dynamically, and in the process of work I assign them the necessary handlers. Much of the code is omitted because it follows the same principles as the above. Of course, in the data module you need to implement saving changes, etc., but I think that you can cope with this yourself. No big deal. In addition, if you noticed, database requests are loaded from files. I think this approach is more practical when requests are stored separately from the application. Although of course it still depends on the specific tasks and on your preferences.

I hope that this material was useful - if you have any questions, ask in the comments.