In Part I we looked at simple SDI forms and considered some good reasons for not letting your program auto-create forms. This instalment builds on that to demonstrate techniques available
when closing modal forms and how one form can retrieve user input or other data from a secondary form.
Finding out how a modal form was closed
Modal forms offer specific features that we cannot have when displaying non-modally. Most
commonly, we will display a form modally to isolate its processes from anything that might
otherwise happen on the main form. Once these processes complete, you might want to know
whether the user pressed the Save or Cancel button to close the modal form.
You can write some interesting code to accomplish this, but it does not have to be difficult. Delphi supplies modal forms with the ModalResult property, which we can read to tell how the
user exited the form.
The following code returns a result, but the calling routine ignores it:
var
F:TForm2 ;
begin
F := TForm2.Create(nil) ;
F.ShowModal ;
F.Release ;
The example just shows the form, lets the user do something with it, then releases it. To
check how the form was terminated we need to take advantage of the fact that the ShowModal
method is a function that returns one of several ModalResult values. Change the line
F.ShowModal to
if F.ShowModal = mrOk then
...
We need some code in the modal form to set up whatever it is we want to retrieve. There is more
than one way to get the ModalResult because TForm is not the only component having a ModalResult
property - TButton has one too.
Let us look at TButton's ModalResult first. Start a new project, and add one additional form.
Next add a TButton to the main form, double click the new button and enter the following code:
procedure TForm1.Button1Click(Sender: TObject);
var
f:TForm2 ;
begin
f := TForm2.Create(nil) ;
try
if f.ShowModal = mrOk then
Caption := 'Yes'
else
Caption := 'No' ;
finally
f.Release ;
end ;
end ;
Now select the additional form. Give it two TButtons, labelling one 'Save' and the other 'Cancel'.
Select the Save button and press F4 to bring up the Object Inspector, scroll up/down until you
find the property ModalResult and set it to mrOk. Go back to the form and select the Cancel button, press F4, select the property ModalResult, and set it to mrCancel.
It's as simple as that. Now press F9 to run the project. (Depending on your environment settings, Delphi may prompt to save the files.) Once the main form appears, press the Button1 you added earlier, to show the child form. When the child form appears press the Save button and the form closes, once back to the main form note that it's caption says "Yes". Press the main form's button to bring up the child form again but this time press the Cancel button (or the System menu Close item or the [x] button in the caption area). The main form's caption will read "No".
How does this work? To find out take a look at the Click event for TButton (from StdCtrls.pas):
procedure TButton.Click;
var
Form: TCustomForm;
begin
Form := GetParentForm(Self);
if Form <> nil then
Form.ModalResult := ModalResult;
inherited Click;
end;
What happens is that the Owner (in this case the secondary form) of TButton gets its ModalResult set according to the value of the TButton's ModalResult. If you don't set TButton.ModalResult, then the value is mrNone. Even if the TButton is placed on another control the parent form is still used to set its result. The last line then invokes the Click event inherited from its ancestor class.
To understand what goes on with the Forms ModalResult it is worthwhile reviewing the code in Forms.pas, which you should be able to find in ..\DelphiN\Source (where N represents the version number.
In TForm's ShowModal function, directly after the form is shown, a Repeat-Until loop starts, which keeps checking for the variable ModalResult to become a value greater than zero. When this occurs, the final code closes the form.
You can set ModalResult at design-time, as described above, but you can also set the form's ModalResult property directly in code at run-time:
Points of Interest
Events to look at for ModalResult (in Forms.pas):
function TCustomForm.ShowModal: Integer;
procedure TCustomForm.CloseModal;
function TCustomForm.CloseQuery: Boolean;
TCustomForm.Close;
ModalResult Constants from Controls.pas
const
mrNone = 0;
mrOk = idOk;
mrCancel = idCancel;
mrAbort = idAbort;
mrRetry = idRetry;
mrIgnore = idIgnore;
mrYes = idYes;
mrNo = idNo;
mrAll = mrNo + 1;
mrNoToAll = mrAll + 1;
mrYesToAll = mrNoToAll + 1;
Now, knowing whether the user wants to accept or reject what occurred on the child form, the calling form can react by using the information from the child form or ignoring it. Sometimes you will need to get more than one piece of information back from a form, but let's start simple. The following example shows how to get the value stored in a TEdit text property if the user pressed the Ok button on the child form.
// Code for main form
procedure TfrmMain.ModalResultdemo2Click(Sender: TObject);
var
f:TfrmSimpleString ;
begin
f := TfrmSimpleString.Create(nil) ;
try
if f.ShowModal = mrOk then
ShowMessage( f.Edit1.Text ) ;
finally
f.Release ;
end ;
end;
To keep the example short we are just checking for an Ok response and ignoring negative responses. When the user presses the OK button (its ModalResult is set to mrOk) the following code reads the value of an edit control, Edit1, on the called form and then destroys the form. In a real application you might need information from several components on the called form. We could have streamlined the code a little more by adding code to let the user choose whether to continue with closing the form or aborting back into the called form:
// Code for main form
procedure TfrmMain.ModalResultdemo2Click(Sender: TObject);
var
f:TfrmSimpleString ;
S:String ;
begin
f := TfrmSimpleString.Create(nil) ;
try
f.GetThatString(S) ;
ShowMessage(s) ;
finally
f.Release ;
end ;
end;
//
// Code for the called form
type
TfrmSimpleString = class(TForm)
Button1: TButton;
Button2: TButton;
Edit1: TEdit;
private
{ Private declarations }
public
{ Public declarations }
procedure GetThatString(var aString: string);
end;
var
frmSimpleString: TfrmSimpleString;
implementation
{$R *.DFM}
procedure TfrmSimpleString.GetThatString(var aString: string);
begin
case ShowModal of
mrOk: aString := 'OK' ;
mrCancel: aString := 'Cancel' ;
end ;
end;
On the called form we add a procedure to interrogate the user modally to find out whether the user wants to continue or abort the current operation. For elegance, the Case statement is used rather than a chain of nested IF..THEN..ELSE statements, i.e.
if ShowModal = mrOk then
...
else if ShowModal = mrCancel then
...
else if ShowModal = mrRetry then
...
More Complex Return Types
Retrieving a string is simple enough but, often, you will need much more than a simple string. Methods
for getting a lot of information back from a called form include using an array, a TList, a stringlist, or records. The logic used to determine if the user wants to return information remains the same, only the means of
storing it varies.
To show how this might be done, I will use a record to store user input for retrieval once the user presses OK in the called form. To make things interesting, we will use an array of records so that
multiple pieces of information can be returned from the called form!
Before digging into an example, lets go through a short lesson of record types for those programmers that may have not worked with records:
Record Types, the Basics
A record is a special kind of user-defined data type. A record is a container for a mixture of related variables of diverse types, referred to as fields, collected into one type. Records
are sometimes called complex types, because they are made up of other data types. Other data types by comparison, are often referred to as simple data types.
I can hear beginners saying "I really don't have a use for them..." or "I will learn them later when I am not so busy". Well, later is not always the best time to learn things, especially when "later" may be the crunch time when unwelcome bugs habitually creep into applications!
Records are commonly used in Microsoft Windows API calls, where they are referred to as quot;structures", which is C/C++ programming language terminology for a very similar thing.
Suppose you are writing an application and you need to determine a form's original state before minimizing or maximizing the form, or get/set the size a form can shrink to or grow to. Some of this can be done
with plain old Delphi code while other parts need to be done using API calls. If you need to restrict form sizing then you are going to have to use WM_GETMINMAXINFO from the API.
As you might have guessed, that uses a record. In Delphi Win32 Help you will find that the record used is defined as:
typedef struct tagMINMAXINFO
{
// mmi
POINT ptReserved;
POINT ptMaxSize;
POINT ptMaxPosition;
POINT ptMinTrackSize;
POINT ptMaxTrackSize;
} MINMAXINFO;
The Delphi architects thoughtfully wrote the interface translation to handle this, but you need to search though the Delphi source code to find the information. The following record is defined in Messages.pas:
TWMGetMinMaxInfo = record
Msg: Cardinal;
Unused: Integer;
MinMaxInfo: PMinMaxInfo;
Result: Longint;
end;
The rest is found in the Windows unit:
{ Struct pointed to by WM_GETMINMAXINFO lParam }
PMinMaxInfo = ^TMinMaxInfo;
TMinMaxInfo = packed record
ptReserved: TPoint;
ptMaxSize: TPoint;
ptMaxPosition: TPoint;
ptMinTrackSize: TPoint;
ptMaxTrackSize: TPoint;
end;
Of course you could say that not much knowledge is needed for accomplishing the task we set up, but what if Delphi didn't have code for the above? Obviously you would have had to write it yourself, and without the proper knowledge it would be impossible to code. At times you need many pieces of information about a form. One of the API calls to acquire the information is called GetWindowPlacement, and to change form stuff that Delphi does not directly handle you might need to call SetWindowPlacement. Both require the use of this record:
typedef struct _WINDOWPLACEMENT
{ // wndpl
UINT length;
UINT flags;
UINT showCmd;
POINT ptMinPosition;
POINT ptMaxPosition;
RECT rcNormalPosition;
} WINDOWPLACEMENT;
Delphi has defined it, but if they had not, we would be translating it ourselves!
type
Tkg_WINDOWPLACEMENT = Record
Length:Integer ;
Flags:Integer ;
ShowCmd:Integer ;
ptMinPosition:Tpoint ;
ptMaxPosition:Tpoint ;
rcNormalPosition:Trect ;
end ;
This demonstrates the importance of knowing how to work with records for API calls. While everyday cases are not obvious, they do arise. One is hinted at in Delphi Help, with an example:
type
TPerson = record
FirstName, LastName: string[40] ;
{ Fixed portion of record begins here }
BirthDate: TDate;
case Citizen: Boolean of
{ variant portion of record begins here }
True: (BirthPlace: string[40]);
False: (Country: string[20]);
EntryPort: string[20];
EntryDate: TDate;
ExitDate: TDate);
end;
end
This variant record has a section (which must follow the fixed section) that can have multiple personalities. In the above example, if Citizen equaled true then we would have:
type
TPerson = record
FirstName, LastName: string[40];
BirthDate: TDate;
BirthPlace: string[40];
end
If Citizen was false:
type
TPerson = record
FirstName, LastName: string[40];
BirthDate: TDate;
Country: string[20];
EntryPort: string[20];
EntryDate: TDate;
ExitDate: TDate;
end
Stacking Records into An Array
Now we have a understanding of records, let's step into another dimension and create an array of records which allows you to store multiple records which can be returned to a calling form. Place the following declaration into the interface section of a form.
TPerson = record
FirstName, LastName: string[40] ;
BirthDate: TDate ;
BirthPlace: string[40] ;
end;
Add a Memo control and a command button to the form, enter the code below:
procedure TForm1.Button1Click(Sender: TObject);
var
MyPeople: Array[0..2] of TPerson ;
i:Integer ;
begin
Memo1.Clear ;
for i := 0 to 2 do
begin
MyPeople[i].FirstName := 'MyPeople[' + IntToStr(i) + '].FirstName' ;
MyPeople[i].LastName := 'MyPeople[' + IntToStr(i) + '].LastName';
MyPeople[i].BirthDate := Now;
MyPeople[i].BirthPlace := 'MyPeople[' + IntToStr(i) + '].BirthPlace';
end ;
for i := 0 to 2 do
begin
with Memo1.Lines do
begin
Add(MyPeople[i].FirstName + ' ' + MyPeople[i].LastName);
Add(DateToStr(MyPeople[i].BirthDate));
Add(MyPeople[i].BirthPlace);
Add('');
end;
end;
end;
Pressing the button populates the array of records with dummy information, then displays the records in the memo control. This should give you a starting point to working with record arrays. The main thing to remember
about the example above is that we created a record type, supplied the name of TPerson, then created a local variable called MyPeople which is an array of type TPerson which can hold three (3) rows of information.
Let's use the idea to present a data entry form to a user, allow data to be entered about several people before returning to the main form to process it. Use the next piece of code to call the secondary form for the
data entry:
TfrmMain.PersonRecord1Click(Sender: TObject);
var
f:TForm5;
begin
f := TForm5.Create(Self);
try
if f.ShowModal = mrOk then
if f.PersonCount > 0 then
ShowMessage('The first person is' + #13 + '"'
+ f.PersonArray[0].FirstName + '" "'
+ f. PersonArray[0].LastName + '"');
finally
f.Release;
end;
end;
When the user invokes the event, the main form creates the data entry form, shows it, then checks to see if there were any people entered into the people array. The variable PersonCount in the data entry form is incremented each time a new person is added to the array of records.
The following code is placed into a simple unit so that any form in the project can see the People record along with a constant used to limit how many elements (records) can be used in the array of records.
unit kg_Globals;
interface
uses
Windows,Dialogs,SysUtils,Messages,Classes,Forms,FileCtrl;
const
ARRAY_SIZE = 5; {Could be a larger size}
type
Tkg_People = Record
FirstName:String;
LastName:String;
Street:String;
City:String;
State:String;
ZipCode:String;
Phone:String;
Email:String;
end;
implementation
end.
(Keep each record member to strings to make the example code easy to understand)
In the public portion of the called form's declarations we declare an array of records and a variable for keeping track of how many records the user has entered.
...
public
{ Public declarations }
PersonArray: Array [0..ARRAY_SIZE] of Tkg_People;
PersonCount: Integer;
In the Create event of the data entry form the following code is called to clear all edit controls so that the form starts up with a clean slate:
procedure TForm5.ClearEdits;
var
i:Integer ;
begin
{ sets each TEdit control's text property to an empty string }
for i := 0 to ComponentCount -1 do
if (Components[i] is TEdit) then
TEdit(Components[i]).Text := '' ;
end;
(Each time the user accepts a screenful of people information the ClearEdit procedure is triggered)
The next code fills a row of data in the people array and increments the row counter. The counter is checked to ensure that the number of rows keeps within the fixed boundaries of the array of records.
procedure TForm5.cmdAddPersonClick(Sender: TObject);
begin
{ Populate a record in an element in the array of records with current textbox values }
PersonArray[PersonCount].FirstName := First.Text;
PersonArray[PersonCount].LastName := Last.Text;
PersonArray[PersonCount].Street := Street.Text;
PersonArray[PersonCount].City := City.Text;
PersonArray[PersonCount].State := State.Text;
PersonArray[PersonCount].ZipCode := Zip.Text;
PersonArray[PersonCount].Phone := Phone.Text;
PersonArray[PersonCount].Email := Email.Text;
Inc(PersonCount);
{ Once the maximum elements are used up, disable this button.
NOTE: ARRAY_SIZE is a user defined constant created for this demo.
The Constant indicates how many records can be accessed in the array. }
TButton(Sender).Enabled := PersonCount < ARRAY_SIZE;
{ If we can still add more persons clear old entries, place
focus on "First" name text control }
if TButton(Sender).Enabled then
begin
ClearEdits;
First.SetFocus ;
end
else
{ Exit time, give a hint by placing focus on the exit button }
cmdCloseForm.SetFocus ;
end;
Using this example you can return just about any type of data, in either single or multiple rows.
To recap, you can detect how a modal form was closed and get information back in several ways. Keep in mind there are other ways to accomplish returning information from modal forms.
Detour
The last thing a programmer needs when searching for assistance in a help file or manuals is an incorrect example of how to do a particular task. I caught one in D4 after writing this article, to do with "Passing additional arguments to forms", listed under "forms" in Delphi help. Here are the key pieces:
TResultsForm = class(TForm)
ResultsLabel: TLabel;
OKButton: TButton;
procedure OKButtonClick(Sender: TObject);
private
public
constructor CreateWithButton(whichButton: Integer; Owner: TComponent);
end;
constructor CreateWithButton(whichButton: Integer; Owner: TComponent);
begin
case whichButton of
1: ResultsLabel.Caption := 'You picked the first button.';
2: ResultsLabel.Caption := 'You picked the second button.';
3: ResultsLabel.Caption := 'You picked the third button.';
end;
end;
procedure TMainForm.SecondButtonClick(Sender: TObject);
var
rf: TResultsForm;
begin
rf := TResultsForm.CreateWithButton(2, self);
rf.ShowModal;
rf.Free;
end;
Don't feel bad if you can not figure out what's missing/wrong with the example. This code was posted on Borland's news group as a solution for a posted question. Instead of trying to figure out the errors, look at the correct code:
type
TResultsForm = class(TForm)
ResultsLabel: TLabel;
OKButton: TButton;
private
public
constructor Create(whichButton: Integer; Owner: TComponent); reintroduce ;
end;
constructor TResultsForm.Create(whichButton: Integer; Owner: TComponent);
begin
inherited Create(Owner) ;
case whichButton of
1: ResultsLabel.Caption := 'You picked the first button.';
2: ResultsLabel.Caption := 'You picked the second button.';
3: ResultsLabel.Caption := 'You picked the third button.';
end;
end;
procedure TMainForm.SecondButtonClick(Sender: TObject);
var
f: TResultsForm ;
begin
f := TResultsForm.Create(2, self);
try
f.ShowModal;
finally
f.Release ;
end;
end;