Placing Forms On Forms - MDI Replacement

Creating Child Forms

The company I work for is in the business of providing business
intelligence solutions for fortune 500+ companies.  Our applications are
typically targeted to the upper strata of user.  That is, they are mainly
targeted to users that are one step removed from computer-phobic.  As
such, our applications must be able to present data to executives in a
non-threatening way.

For example, our target audience is not thrilled with menu bars and tiny
icons.  Rather, the approach we take with our applications is to provide
big, fat buttons with very descriptive icons, lots of flying help, etc.  
And the navigation between parts of the application is done via a set of
Notebook tabs.  To achieve the look we want, we needed a way to create
forms in regions on the screen.  That is, we desired to create a form
between the huge button bar at the top, and the status bar at the bottom.

Our initial swipe at this problem led us down the path of MDI.  MDI
seemed to be the perfect solution to our needs.  We were able to create
the toolbar and the status bar on the MDI Parent.  Then, all of our child
forms could be created and maximized so they consumed the entire client
area.  Development continued until we demonstrated to another member of
the team.

Immediately, he complained about the way the forms were created.  With
MDI, when the form is created, Windows (or perhaps Delphi) chooses a
default location for the child (tiled so to speak).  When Windows figures
out that the form should be maximized, it is done in a separate step.  
Thus, the form is seen in one place, then stretched to fill out the
client area.

The other problem noted was the caption in the main Title bar (the
caption of the MDI parent).  When a MDI child is maximized, the caption
is no longer visible.  So, the caption is moved into the MDI parent in
square brackets ([ ]).  No amount of fiddling would remove the square
brackets.

We never mentioned the problems with hiding an MDI child since it seemed
inevitable that we would have to provide a better solution.  But, in case
you didnt already know, MDI children cannot be hidden.  They can be
minimized or destroyed, but not hidden.  There is also speculation that
Microsoft has abandoned it in favor of SDI (Single Document Interface)
for Windows 95.

This was the impetus for me to find a way to parent a form to another
form.  As we all know, when controls are instantiated, they have to be
parented to a window for them to be visible.   My first step then was to
create two forms - one big and one small.  The smaller was to be parented
to the larger by a push-button.  The following was the push-button code:

procedure TForm1.BitBtn1Click(Sender: TObject);
begin
        form2 := TForm2.Create(Self);
        form2.parent := self;
        form2.visible := true;
end;

Of course, Form2 was removed from the Auto-Create forms setting.  This
resulted in Form2 being created, but not parented to Form1 the way a
control is.  As a matter of fact, it acted just like a modal dialog in
that clicking on Form1 activated Form2.  Form1 was totally disabled.

This wasnt the result I was looking for at all.  I wanted a form that
acted like a component.  Well, it was finally time to delve into the VCL.
 I decided that something strange must be done in the constructor of a
TForm to cause this behavior.  My rationale was that Forms are derived
from TScrollingWinControl, as are TScrollBoxes.  So if a TScrollBox can
be easily dropped on a form, why cant a Tform?

My first stop was TForms.Create.  Here is the constructor as it appears
in FORMS.PAS.  I have added all comments.

constructor TForm.Create(AOwner: TComponent);
begin
        { See below for description }
        CreateNew(AOwner);
        { If subclass of Tform then }
        if ClassType <> TForm then
        begin
                {  Set Form State to include fsCreating }
                Include(FFormState, fsCreating);
                try
                        { Read from Resource all components on form }
                        ReadComponentRes(ClassName, Self);
                finally
                        { Done Creating }
                        Exclude(FFormState, fsCreating);
                end;
                try
                        { Call Create Handler}
                        if Assigned(FOnCreate) then FOnCreate(Self);
                except
                        { Oops - Handle Exception }
                        Application.HandleException(Self);
                end;
                { Make form Visible }
                if fsVisible in FFormState then Visible := True;
        end; { IF }
end;

constructor TForm.CreateNew(AOwner: TComponent);
begin
        inherited Create(AOwner);
        ControlStyle := [csAcceptsControls, csCaptureMouse,
csClickEvents,
                csSetCaption, csDoubleClicks];
        Left := 0;
        Top := 0;
        Width := 320;
        Height := 240;
        Visible := False;
        ParentColor := False;
        ParentFont := False;
        Ctl3D := True;
        FBorderIcons := [biSystemMenu, biMinimize, biMaximize];
        FBorderStyle := bsSizeable;
        FWindowState := wsNormal;
        FIcon := TIcon.Create;
        FIcon.OnChange := IconChanged;
        FCanvas := TControlCanvas.Create;
        FCanvas.Control := Self;
        FPixelsPerInch := Screen.PixelsPerInch;
        FPrintScale := poProportional;
        Screen.AddForm(Self);
end;

The CreateNew method was the first place I dissected.  I spent a great
deal of time trying to figure out if adding Self to Screen was causing
the problem.  I then decided to create my own CreateIt method to see
where the weird behavior was coming from.  Here was the method I created:

constructor TForm2.CreateIt(AOwner: TComponent);
begin
        CreateNew(AOwner);
        try
                ReadComponentRes(ClassName, Self);
        except
                { Ignore - No Error was raised here before }
        end;
        try
                OnCreate(Self);
        except
                { Ignore - OnCreate may not be assigned }
        end;
end;
This method produced the same result as before - Form2 was not properly
parented to Form1.  It also introduced a hindrance of Private sections in
classes.  Since FOncreate is private, I cannot check to see that it is
assigned.  So I am forced to take a more brute force approach.

I then decided to experiment with the placement of the parent statement.
 Clearly when the parent is set after the CreateIt method is executed, I
dont get the result desired.  Perhaps I need to set the parent before
the call to CreateNew?  What do you know?  It worked!  I now had a form
that was parented to the other form.  Form2 always had the appearance of
being disabled (as far as the title bar goes), but for our purposes, that
didnt matter - we werent using title bars on the child window anyway.

Armed with this knowledge, I devised the following constructor for all of
our child forms:

constructor TForm2.Create(AOwner: TComponent);
begin
        if AOwner is TWinControl then
                Parent := AOwner as TWinControl;
        inherited Create(AOwner);
end;

This allows forms to be parented to other forms without the negatives of
 having MDI children.  I also went one step further.  I wanted a utility
function that could be called like Application.CreateForm.  So, I devised
ClientCreate.  The fully-commented source to this function is shown
below:

procedure ClientCreate(FormClass: TFormClass; var Reference; newOwner:
TWinControl);
var
        Form: TForm;
begin
        { Show Hourglass }
        Screen.Cursor := crHourglass;
        { Create form instance }
        Form := FormClass.CreateNew(newOwner);
        try
                { Set variable to instance }
                TForm(Reference) := Form;
                { Set parent before reading component resource }
                Form.Parent := newOwner;
                { Read component resource }
                ReadComponentRes(FormClass.ClassName, Form);
                { Align to client }
                Form.Align := alClient;
                try
                        { Try to execute OnCreate handler }
                        Form.OnCreate(newOwner);
                except
                        { Do Nothing - OnCreate not assigned }
                end;
                { Make form visible }
                Form.Visible := true;
        except
                { Oops - Exception creating form instance }
                TForm(Reference) := nil;
                Form.Free;
                raise;
        end;
        { Restore Cursor }
        Screen.Cursor := crDefault;
end;

It was apparent as I went along that the method that was causing the
un-desirable behavior was related to reading the component from the
resource.  However, I never took the time to track down the exact failure
mechanism.  I hope the process that I went though was helpful to others
in examining the VCL.

--
************************************
*           The Nomad              *
*        tno...@digital.net        *
************************************