April 1999
Version: 1.0, 3.0, and 4.0

Transferring form data

by Kent Reisdorph

Any serious Windows application will have one or more forms. Most forms are used to retrieve information from the user (aside from simple forms like an About form). Your program must have some way of extracting information from the form and, in some cases, sending information to the form. This article will discuss various ways of transferring information from the main form to a secondary form, and vice versa.

Simple forms: one-way data

In the case of a simple form, you don't really need to do much work to transfer data. Let's use a password form as an example. Invoking this type of form from the main form and transferring data might look something like this:
PWForm->ShowModal();
if (PWForm->PWEdit->Text == "bubba")
	// password failed
else
	// password succeeded
Here, we read the value of the password edit component directly through the password form's pointer. This is a one-way transfer of data because we're only reading the data on the form and aren't making any attempt to set data on the form. This situation is only slightly more complicated when you take control of creating the form yourself (in cases where the form isn't auto-created at application startup). Here's how the code might look:

 

TPWForm* form = new TPWForm(this);
form->ShowModal();
if (form->PWEdit->Text == "bubba")
	// password failed
else
	// password succeeded
delete form;
The only item of significance here is that you must remember to extract any data from the form before you delete the form's pointer. Once the form's pointer has been deleted, any attempts to access the pointer will result in an access violation or, in extreme cases, a complete system crash. By the way, I never let the VCL auto-create my forms in real-world applications. I always allocate the memory for the form just before I show the form and free that memory as soon as I'm done with the form.

 

Simple forms: two-way data

In the previous example, we only transferred data one way--from the form to the main form. In many circumstances, you'll need to assign values to controls on a form before the form is shown, and retrieve the values for those controls when the form is closed.

Let's say, for example, that you had an application with an Options form. The Options form, naturally, would allow the user to specify options that control how the application behaves. For the sake of argument, let's say you had an application that dials a remote machine in order to transfer some data via a modem. The user options for this type of program might include the phone number to dial, the name of the TAPI device used to make the connection, and the number of times to retry dialing if the number is busy. These settings would be stored in the Registry, in an INI file, or in a user-defined configuration file. The options would be read from the storage medium to variables in the main form's class at application startup. When the application closes, the options would be saved to persistent storage for retrieval the next time the application runs. Transferring the data to and from the Options form will look something like this:

TOptionsForm* form = new 
	TOptionsForm(this);
form->PhoneNumber->Text = PhoneNumber;
form->TapiDevice->Text = TapiDevice;
form->DialRetries->Text = DialRetries;
if (form->ShowModal() == mrOk) {
	PhoneNumber = form->PhoneNumber->Text;
	TapiDevice = form->TapiDevice->Text;
	DialRetries = 
		form->DialRetries->Text.ToInt();
}
delete form;
Here, we're only dealing with three controls on the form, and yet the code already looks messy. If you had a large number of components on a form, the code would be even more unwieldy. Plus, we aren't showing the code required to save and load the data from persistent storage. That's a lot of code in the main unit of your application that probably doesn't need to be there at all. Now, let's look at an alternative to this method of data transfer.

Encapsulation

Before going on to describing a better method of data transfer, a few words about encapsulation are warranted. Encapsulation is one of those Object Oriented Programming (OOP) terms that we're all familiar with. Encapsulation basically means using an object to perform a specific task whenever possible. The object takes the code required to carry out that task and raises it to a level that's much easier to deal with.

In the context of this article, encapsulation means that you should, whenever possible, write your forms so that they're completely self-contained and don't need to have interaction with the main form at all. This is the ideal way to deal with forms in your applications. There are many types of forms, however, where this approach isn't practical because data needs to be transferred between the main form and the form.

Data transfer the OOP way

Let's go back to our previous example of an application that dials a remote machine. How can we use encapsulation to make the code cleaner? One way is to create a class that holds the options for the application. The class could have data members for each of the application's options. Further, the class could contain methods to save the options to the Registry or to a file and to read those options back again when needed. An instance of the class can be created in the main form so that the main form has access to the options. A pointer to the class can be passed to the Options form so that it also has access to the options. Setting up this type of arrangement requires several steps. We'll examine each of these steps in the following sections.

 

Create a transfer class

The first step is to create a class that will serve as the transfer mechanism for your Options form. First, let's look at the declaration for the class:
class TAppOptions {
	public:
		String PhoneNumber;
		String TapiDevice;
		int DialRetries;
		TAppOptions();
		LoadFromRegistry();
		SaveToRegistry();
};
As you can see, we've created data members for each option on the Options form. We've also added methods to load from the Registry and save to the Registry. Note that this class violates OOP principles by making all data members public, but for simple examples, this is an acceptable practice. An instance of this class will be created in the main form of the application. Typically, this would be done at application startup in either the main form's constructor or in its OnCreate event handler. At creation time, the previous options could be read from persistent storage. For example:

 

void __fastcall 
TMainForm::FormCreate(TObject *Sender)
{
	Options = new TAppOptions;
	Options->LoadFromRegistry();
}
The application's options are now loaded into the class represented by the Options variable and are available for the main form's use. The Options variable is declared as a member of the main form's class. Naturally, you'll want to destroy the TAppOptions instance before the application terminates. Either the main form's destructor or the OnDestroy event handler are logical places for this code. Before you destroy the object, however, you'll want to save the current options to persistent storage. Here's how the code would look when placed in an OnDestroy event handler:

 

void __fastcall 
TMainForm::FormDestroy(TObject *Sender)
{
	Options->SaveToRegistry();
	delete Options;
}
Using this technique, the application's options are always loaded at application startup and saved again at application termination. Alternatively, you could place the code to load the options from persistent storage in the class constructor and the code to save the options in the class destructor.

Modify the Option form's class

In order to pass an instance of the TAppOptions class to your Options form, you'll need to modify the Options form's constructor. Simply add a pointer to the TAppOptions class to your constructor. Your Options form class must also contain a TAppOptions pointer so you have access to the instance throughout the class. The modified TOptionsForm class looks like this (the form's component declarations have been removed to save space):
class TOptionsForm : public TForm
{
__published:	// IDE-managed Components
	// component declarations here
private:	// User declarations
	TAppOptions* Options;
public:		// User declarations
	__fastcall TOptionsForm(
		TComponent* Owner, TAppOptions* 
			options);
};
Notice that we've added a TAppOptions pointer to the private section of the class. Notice, also, that the declaration for the constructor has been expanded to allow us to pass a TAppOptions pointer from the main form. Let's skip ahead just a bit and explain how the Options form will be used in the main form. Using the modified TOptionsForm class requires that you create an instance of the Options form at runtime. Since the Options form now has a specialized constructor, you can't rely on the VCL's auto-creation feature for forms. Here's how the code in the main form would look:

 

void __fastcall 
TMainForm::Options1Click(TObject *Sender)
{
	TOptionsForm* form =
		new TOptionsForm(this, Options);
	form->ShowModal();
	delete form;
}
This code assumes that the Options variable had already been instantiated as discussed in the previous section.

Writing the Options form constructor

Next, we must write the body of the TOptionsForm constructor. First, we assign the TAppOptions pointer passed in the constructor to our internal Options variable. We save the pointer because we need access to it later when the form is closed. Then, we assign values contained in the Options class to the various controls on the Options form. Here's how the constructor looks:
__fastcall TOptionsForm::TOptionsForm(
	TComponent* Owner, TAppOptions* options)
	: TForm(Owner)
{
	Options = options;
	PhoneNumberEdit->Text =
		Options->PhoneNumber;
	TapiDeviceEdit->Text =
		Options->TapiDevice;
	DialRetriesEdit->Text =
		Options->DialRetries;
}
As you can see, we simply take each data member in the Options class and assign its value to a corresponding component on the Options form. This part of the operation is fairly simple.

Saving the new options

Now, we need to take some action when the form is closed. A form like an Options form can be closed in one of two ways--with the OK button or with the Cancel button (the form's close box is the same as clicking the Cancel button). Naturally, we need to account for both possibilities. If the form is closed with the Cancel button, then no further action needs to be taken. The user can change any or all fields on the Options form, but if the Cancel button is pressed, all those changes are effectively abandoned. By simply not acting, we already have accounted for the user closing the form with the Cancel button or with the form's close box.

If the form is closed with the OK button, on the other hand, we need to act to ensure that the new options are implemented. We simply provide an OnClick event handler for the OK button and copy data from the form's controls to the Options class instance. This is essentially the opposite of the code we wrote for the form's constructor. For example:

 

void __fastcall
TOptionsForm::OKBtnClick(TObject *Sender)
{
	Options->PhoneNumber =
		PhoneNumberEdit->Text;
	Options->TapiDevice =
		TapiDeviceEdit->Text;
	Options->DialRetries =
		DialRetriesEdit->Text.ToIntDef(0);
	Close();
}
Since our local Options variable is simply a pointer to the instance created in the main class, this code updates the main class' instance directly. When the Options form closes, the main form's Options variable already contains the new options. The main form doesn't have to change at all to account for the new options. This is where encapsulating the application's options in the Options form has great benefit. The Options form does all the work of updating the options and the main form doesn't have to do any extra processing. If there's a drawback to this mechanism, it's that you need to create a transfer class for each of your application's forms. Still, this method is preferred over those discussed earlier.

Example

Transferring data to and from your forms is something that's required in nearly every non-trivial C++Builder application. Our example application for this article transfers data to and from an options form using a transfer class. Listing A contains the header for the application's main form. Listing B contains the source unit for the main form. Listing C shows the header for the application's Options form and Listing D shows the source unit for the Options form. We've placed the entire implementation of the TAppOptions class in the header for the Options form to save space. Normally, however, you'd place this class in its own unit.

Listing A: MAINU.H

//---------------------------------------------
#ifndef MainUH
#define MainUH
//---------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <Menus.hpp>
#include "OptionsU.h"
//---------------------------------------------
class TMainForm : public TForm
{
__published:	// IDE-managed Components
	TMainMenu *MainMenu1;
	TMenuItem *File1;
	TMenuItem *Exit1;
	TMenuItem *Tools1;
	TMenuItem *Options1;
	void __fastcall FormCreate(TObject *Sender);
	void __fastcall FormDestroy(TObject *Sender);
	void __fastcall Exit1Click(TObject *Sender);
	void __fastcall
		Options1Click(TObject *Sender);
private:	// User declarations
	TAppOptions* Options;
public:		// User declarations
	__fastcall TMainForm(TComponent* Owner);
};
//---------------------------------------------
extern PACKAGE TMainForm *MainForm;
//---------------------------------------------
#endif
Listing B: MAINU.CPP
//---------------------------------------------
#include <vcl.h>
#pragma hdrstop

#include "MainU.h"
//---------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TMainForm *MainForm;
//---------------------------------------------
__fastcall
TMainForm::TMainForm(TComponent* Owner)
	: TForm(Owner)
{
}
//---------------------------------------------
void __fastcall
TMainForm::FormCreate(TObject *Sender)
{
	// Create an instance of TAppOptions and
	// load from the registry.
	Options = new TAppOptions;
	Options->LoadFromRegistry();
}
//---------------------------------------------
void __fastcall
TMainForm::FormDestroy(TObject *Sender)
{
	// Save the options to the registry and
	// then delete the TAppOptions object.
	Options->SaveToRegistry();
	delete Options;
}
//---------------------------------------------
void __fastcall
TMainForm::Exit1Click(TObject *Sender)
{
	Close();
}
//---------------------------------------------
void __fastcall
TMainForm::Options1Click(TObject *Sender)
{
	// Create an instance of the TOptionsForm
	// class, passing the Options variable as
	// a paramter. TOptionsForm does the rest.
	TOptionsForm* form =
		new TOptionsForm(this, Options);
	form->ShowModal();
	delete form;
}
//---------------------------------------------
Listing C: OPTIONSU.H
//---------------------------------------------
#ifndef OptionsUH
#define OptionsUH
//---------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <Registry.hpp>
//---------------------------------------------
// The registry key where we will save the data.
const String RegKey =
	"Software\\ZDJournals\\DialogTransferEx";

// The TAppOptions class for transferring data.
class TAppOptions {
	public:
		String PhoneNumber;
		String TapiDevice;
		int DialRetries;
		TAppOptions() {
			PhoneNumber = "";
			TapiDevice = "";
			DialRetries = 0;
		}
		LoadFromRegistry() {
			TRegistry* reg = new TRegistry;
			reg->OpenKey(RegKey, true);
			if (reg->ValueExists
					("PhoneNumber")) {
				PhoneNumber =
					reg->ReadString
					("PhoneNumber");
				TapiDevice =
					reg->ReadString
					("TapiDevice");
				DialRetries =
					reg->ReadInteger
					("DialRetries");
			}
			delete reg;
		}
		SaveToRegistry() {
			TRegistry* reg = new TRegistry;
			reg->OpenKey(RegKey, true);
			reg->WriteString(
				"PhoneNumber", PhoneNumber);
			reg->WriteString(
				"TapiDevice", TapiDevice);
			reg->WriteInteger(
				"DialRetries", DialRetries);
			delete reg;
		}
};

class TOptionsForm : public TForm
{
__published:	// IDE-managed Components
	TLabel *Label1;
	TLabel *Label2;
	TLabel *Label3;
	TEdit *PhoneNumberEdit;
	TEdit *TapiDeviceEdit;
	TEdit *DialRetriesEdit;
	TButton *OKBtn;
	TButton *CancelBtn;
	void __fastcall
		CancelBtnClick(TObject *Sender);
	void __fastcall OKBtnClick(TObject *Sender);
private:	// User declarations
	TAppOptions* Options;
public:		// User declarations
	__fastcall TOptionsForm(
		TComponent* Owner, TAppOptions* options);
};
//---------------------------------------------
extern PACKAGE TOptionsForm *OptionsForm;
//---------------------------------------------
#endif
Listing D: OPTIONSU.CPP
//---------------------------------------------
#include <vcl.h>
#pragma hdrstop

#include "OptionsU.h"
//---------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TOptionsForm *OptionsForm;
//---------------------------------------------
__fastcall TOptionsForm::TOptionsForm(
	TComponent* Owner, TAppOptions* options)
	: TForm(Owner)
{
	// Save the incoming TAppOptions pointer.
	Options = options;
	// Transfer data from the Option class
	// to the individual controls on the form.
	PhoneNumberEdit->Text =
		Options->PhoneNumber;
	TapiDeviceEdit->Text =
		Options->TapiDevice;
	DialRetriesEdit->Text =
		Options->DialRetries;
}
//---------------------------------------------
void __fastcall
TOptionsForm::CancelBtnClick(TObject *Sender)
{
	Close();
}
//---------------------------------------------
void __fastcall
TOptionsForm::OKBtnClick(TObject *Sender)
{
	// Transfer data from the individual controls
	// to the Options class.
	Options->PhoneNumber =
		PhoneNumberEdit->Text;
	Options->TapiDevice =
		TapiDeviceEdit->Text;
	Options->DialRetries =
		DialRetriesEdit->Text.ToIntDef(0);
	Close();
}
//---------------------------------------------