Computing: Free Pascal Programming

Programming Home   Home   Contact

Exchanging data between two forms.

There are multiple situations, where your application needs to include more than one form, the most common case being, where the major part of the application runs in one window, and you want to use another window to do a specific task. For example, in my "Bacteria1" and "Conics" applications, I open a second window, to display the bacteria growth graph resp. to draw the conic. Sometimes, you may want to use a separate window associated with each of several things, you do in your application. Thus, in my "Pulleys" program, I use a separate form for each kind of exercise. The advantage of doing so, is that you will not have to change the controls' properties (visible or not, caption, size and position) depending on what is given and what is asked in the exercise. It also regroups all the code, relative to a given exercise in a different unit. The disadvantage, of course, is that this considerably increases the size of the executable. A big issue, years ago, but not really a big deal nowadays, where memory is at affordable price and where, anyway, the operating system itself needs gigabytes of RAM. Another situation, where you may want to use a second form, is when you want to use all the place of the main window to show the application's result and thus, want to enter the input data in another window; an example of this is my "DCircuits2" application, where I use a data form, where the user can enter the circuit parameters.

The first thing to do in a multiple form application is to declare the secondary form units in your main unit. This is done the same way, as you do with any unit. Here the uses clause of my "Bacteria1" program; the unit bacteria1_graph being associated with the form fBacteria1G (display of the graph) and bacteria1_help being associated with the form fBacteria1H (display of the application help text):
        Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs,
        ExtCtrls, Menus, StdCtrls, PopupNotifier, bacteria1_graph, bacteria1_help;

Doing this, a secondary window may be opened by the Show or ShowModal method. The difference between the two methods is, that when using ShowModal, the code in the main unit is paused, until the secondary window is closed. Thus, in my "Bacteria1" application, the code
lets the main window accessible, when the graph is displayed. This means, that the user may enter new bacteria data (and displaying the graph), without having to close the graph window. On the other side, in my "DCircuits2" application, I use ShowModal: the application is paused (with the main window being inactive), until the user, after having entered the transistor data, has closed the data entry window.

If the main unit has access to the secondary unit's form, it has access to this form's controls, their properties and their methods. This makes it possible to directly modify the secondary window's content. Two possibilities for you, to choose from, to code a multiple form application:

Both of these ways to proceed have their advantages and disadvantages and I sometimes use the one, sometimes the other. In my "Bacteria1" application, for example, the complete drawing of the graph is done in the main unit (and the code in the bacteria1_graph unit is limited to create the bitmap, associated with the image object and the form "Close" method to close the window). In the "Conics" application, I chose the second way to proceed and there is a good reason to do so: The fGraph form contains a zoom function to alter the actual scaling of the conic's curve. Thus, obviously, you have to place the drawing code into the conics_graph unit.

Accessing controls and variables on the secondary form.

All controls on the secondary form are automatically accessible from the main unit, if the secondary unit is declared with the uses clause. Here some code of the main unit of my "DCircuits2" application. Depending on the options selected by the user (consider or not the base-emitter voltage), the transistor materials comboboxes (to choose between silicon and germanium) on the data entry form, called fData, have to be enabled or not, and if so, only for the number of transistors actually selected on the data entry form.
    if mSettingsVoltage.Checked then begin
        fData.cbMaterial1.Enabled := False;
        fData.cbMaterial2.Enabled := False;
        fData.cbMaterial3.Enabled := False;
    else begin
        fData.cbMaterial1.Enabled := True;
        if fData.rbTransistors2.Checked or fData.rbTransistors3.Checked then
            fData.cbMaterial2.Enabled := True;
        if fData.rbTransistors3.Checked then
            fData.cbMaterial3.Enabled := True;

Variables of the secondary unit may be accessed the same way, under condition to be declared as public variables. Here some code of my "Conics" application: When the button "Draw" is pushed on the main form, the conics parameters (entered on the main form), have to be passed to the fGraph form and this form has to be shown (with the code, that actually draws the conic in the TfGraph.FormActivate method).
    // Pass conic parameters to fGraph form
    fGraph.sConic := sConic;
    fGraph.sEquation := sEquation;
    fGraph.rA := rA; fGraph.rB := rB;
    fGraph.bInverse := cbInverse.Checked;
    // Show fGraph window (conic will be drawn with actual parameters at window show-up)

To read any user entry data or calculation results from the secondary unit into the main unit, proceed the same way. Example: Reading the transistor characteristics from the controls on the fData form into variables of the main unit.
    if fData.sButton = 'ok' then begin
        if fData.rbTransistors1.Checked then
            iCircuit := 1
        else if fData.rbTransistors2.Checked then
            iCircuit := 2
            iCircuit := 3;
        rVS := StrToFloat(fData.edVoltage.Text);
        if fData.edCurrent.Text <> '' then
            rIL := StrToFloat(fData.edCurrent.Text)
            rIL := rVS / StrToFloat(fData.edResistance.Text);
        iBeta1 := StrToInt(fData.edBeta1.Text);
        iBeta2 := 0; iBeta3 := 0;
        if fData.edBeta2.Enabled then
            iBeta2 := StrToInt(fData.edBeta2.Text);
        if fData.edBeta3.Enabled then
            iBeta3 := StrToInt(fData.edBeta3.Text);
        sMaterial1 := fData.cbMaterial1.Text;
        sMaterial2 := fData.cbMaterial2.Text;
        sMaterial3 := fData.cbMaterial3.Text;


You may wonder, what's this fData.sButton variable is about. The data entry form has two buttons: If the user pushes btOK, the transistor data should be returned to the main form and the calculations should be done; if she pushes btCancel, the entry should simply be ignored. As I didn't find a direct way to check which button has been pressed on the secondary form, I proceeded as follows: Creating a public string variable (fData.sButton), that is set to "ok" in the TfData.btOKClick method and is set to "cancel" in the TfData.btCancelClick method. Clicking any of these buttons closes the data entry window and returns the control to the main unit. To know, if btOK or btCancel was pushed, just read the value of the sButton variable...

Passing data to a secondary form at application start.

In my Luxembourg1 application, I read the data, concerning the Luxembourger cantons and townships from a text file. The array with the canton data should be passed to the fEmblems form, that is used to display the emblem images, the user has to choose from the one, that corresponds to the canton name displayed during the canton emblem quiz. Such tasks are part of the application initialization and should be done at application start, i.e. placed in the TfLuxbg1.FormCreate method. Have a look at the screenshot below. Do you guess, what I did wrong and why this external error occurs?

External error when passing data to a secondary form at program start-up

There isn't any error in the code, but the code is placed in the bad method! Passing data to a secondary form can't be coded in the main unit's FormCreate method!. The reason is simple: The different forms of an application are created one after the other, starting with the main form. Creating the form consists of executing all code in the FormCreate method, thus in our example, including the data passage to the secondary form. You see now, what's the problem? With the data passage code in the FormCreate method, you try to access the public array on the fEmblems form at a moment, where this form does not yet exist. Thus, obvious that this is not possible!

A simple work-around is to place code, that passes the data, into the FormActivate method. The (main) window only shows up, when all forms have been created, thus, accessing the fEmblems.aCantons array, when the main form is activated, doesn't cause any problem. A little issue, however: The initialization of the cantons array on the fEmblems form should be done at application start and not every time, when the window with the emblem images closes and the main window becomes active again. As for the button, I did not find a direct way to determine application start-up and so I used, here too, a help variable. Setting the Boolean bStart to True in the FormCreate method and executing the code in the FormActivate method only if bStart actually is True, that's what we want, isn't it? Provided, that we don't forget to set bStart to False, after the array on fEmblems has been initialized. Here's the code:
  procedure TfLuxbg1.FormCreate(Sender: TObject);
    ReadTownships(aCantons, aTownships);
    SetLength(bTownshipsDone, Length(aTownships));
    bStart := True;    // start of application flag (used in FormActivate method)
  procedure TfLuxbg1.FormActivate(Sender: TObject);
    if bStart then begin
      fEmblems.aCantons := aCantons;
      bStart := False;

Note: You can also use the work-around with the Boolean variable to set the focus to a given control at application start. In fact, here too, you may encounter the problem, that this control is not yet ready to be accessed (and you get a "Can't focus" error, when running the application).