Hosting forms within a main form

by Kent Reisdorph

Almost every serious C++Builder application has secondary forms in addition to the main form. Sometimes these forms are presented to the user in the form of dialogs and other times in the form of modeless windows. The VCL model makes creating and displaying secondary forms easy.

The problem with this ease of use is that sometimes C++Builder programmers have trouble thinking outside the box. Not all applications can benefit from the modeless window model. Some applications need to display various windows—views, if you will—within the main form. This article will explain how to host a secondary form within your main form. The secondary form will appear to be part of the main form and the user won't even know that a second form is being shown. Figure A shows a main form with a second form in the client area.

Figure A

A main form hosting a secondary form in the client area.

Understanding the parent/child relationship

The basic idea of this type of application is to make all secondary forms children of the main form. This design is common in other frameworks (such as OWL or MFC), but is not something that you see often in a VCL application. The VCL doesn't allow you to simply set a property in order to make one form the child of the second form. You must do a little work in order to make one form a child of another. (See the sidebar, Understanding VCL ownership and parentage for more information on how the VCL model deals with child windows.)

In order to have one form host another form, you must tell Windows that the secondary form is a child of the main form. In C++Builder programming we tend to think of forms as windows and components as child objects. The truth is, though, that from Windows’ perspective all forms and components are simply windows. You can specify that any window (a form or a component) be the child of another window. You only need to step out of the VCL box for a moment.

The example program’s design

Before I go on, I want to give you some background on the example program for this article. The example, called PARENTING, contains a main form that has a toolbar at the top, and a status bar at the bottom. In addition to the main form, the program has two child forms. One child form, called TTableForm, displays the ANIMALS.DBF table in a grid. The ANIMALS table is one of the sample database tables that ships with C++Builder. A second child form, TChartForm displays the ANIMALS table in a TChart. (My apologies to those of you who are using C++Builder 4 Standard as it does not ship with the database components.)

You can choose to view either the table form or the chart form by selecting an item from the main menu, or by clicking on one of the toolbar buttons. When you select a form to display, the active form, if any, will be destroyed and the selected form displayed. The child form will be displayed in the client area of the main form below the toolbar and above the status bar. As an added feature, the child form is resized to always fill the client area of the main form if the main form is resized.

With that description of the example program behind us, we can move on to a discussion of how to set up the child forms.

Setting up the child forms

One of the advantages to hosting child forms within the main form is that you can design your child form just as you would any other secondary form. That is, you create a new form, add components to it, and write the code for the form. This makes it easy to design the child form, and to keep all the code that drives the child form in one central place.

Overriding the CreateParams() method

As I have said, in order for the main form to host a secondary form you need to set the main form as the secondary form’s parent. This is done by overriding the VCL’s CreateParams() method. CreateParams() is called when the VCL creates the underlying window associated with a form. The declaration for CreateParams() looks like this:

void __fastcall 
  CreateParams(TCreateParams& Params);

As you can see, CreateParams() has a reference to a TCreateParams structure as its only parameter. TCreateParams is defined in the VCL as follows:

struct TCreateParams
{
  char *Caption;
  unsigned Style;
  unsigned ExStyle;
  int X;
  int Y;
  int Width;
  int Height;
  HWND WndParent;
  void *Param;
  tagWNDCLASSA WindowClass;
  char WinClassName[64];
};

This structure contains all the information that Windows needs to create a window. (If you have done Windows programming using the API, you will recognize that the members of the TCreateParams structure map to a Windows CREATESTRUCT structure.) When you override CreateParams() you first call the base class’s CreateParams() method. After that, you can modify the individual members of the TCreateParams structure. Here’s how a basic overridden CreateParams() method might look:

void __fastcall
TChartForm::CreateParams(TCreateParams& Params)
{
  TForm::CreateParams(Params);
  Params.Style = WS_CHILD | WS_CLIPSIBLINGS;
  Params.WndParent = MainForm->Handle;
  Params.X = 0;
  Params.Y = 0;
  Params.Width = MainForm->ClientRect.Right;
  Params.Height = MainForm->ClientRect.Bottom;
}

The key points in the preceding code are the lines that set the Style and WndParent members of the TCreateParams structure. Note that the style is set to a value that includes the WS_CHILD and WS_CLIPSIBLINGS window styles. The WS_CHILD window style specifies that this window is the child of another window. By definition, a child window has no title bar. At design time the child form will have a title bar, but the title bar will be removed when Windows creates the form at runtime. The WS_CLIPSIBLINGS style insures that the various child windows on the main form don’t interfere with one another when the form is painted.

Obviously, a child window must have a parent. You specify the parent by assigning the window handle of the parent window to the TCreateParams structure’s WndParent member. As you can see from the preceding code, the WndParent member is set to the Handle property of the main form. Assigning the parent is relatively straightforward, so I won’t go into further detail on the subject.

Setting the child form’s properties

In addition to the code you see in the CreateParams() method, you must also set some of the child form’s properties. Most of the form’s properties can be left at their default values. You should, however, set the AutoScroll property to false. This assumes, of course, that your form is designed in such a way that scrolling the form will not be necessary. You should also set the Position property to poDefault, since the size and position of the child window will be set in the CreateParams method. The Caption and BorderIcons properties will be ignored so you shouldn’t have to worry about them. Be sure to leave the BorderStyle property set to bsSizeable, and the BorderWidth property set to 0. If you change these properties, the child form won’t fit properly on the main form.

Accounting for other components on the form

In many cases, your main form will contain components besides the secondary form. For example, your main form may have a toolbar and a status bar. In that case, you need to account for the toolbar and status bar when you set the X, Y, Width, and Height members of the TCreateParams structure. The child form must fit between the toolbar at the top of the form, and the status bar at the bottom of the form. Given that, the code that sets the various members of the TCreateParams structure might look like this:

Params.X = 0;
Params.Y = MainForm->ToolBar->Height + 1;
Params.Width = MainForm->ClientRect.Right;
Params.Height = 
    (MainForm->StatusBar->Top - 1) - Params.Y;

Note that the Y member is set to the bottom of the toolbar, plus one pixel. The width of the child form is set to the width of the main form’s client area, and the height of the child form is calculated based on the top of the child window, and the top of the status bar. Basically, the height is set to that part of the main form’s client area that falls between the bottom of the toolbar and the top of the status bar.

That is all that is required to make a secondary form the child of the main form. There are a few other features you may want to implement in the child form, but I’ll save discussion of those features for a bit later.

Setting up the main form

The main form also needs to be set up to handle a secondary form hosted as a child. First, you must remove the child forms from the application’s auto-create list. You will be creating the child forms when needed and don’t want them auto-created. In fact, if you don’t remove the child forms from the auto-create list they will automatically display when the application starts.

You’ll need a variable that keeps track of which child form is currently active. I declared the variable in the main form’s public section as follows:

TForm* ActiveChild;

The ActiveChild variable is public because the child forms need access to the variable. I’ll show you how this variable is used in just a bit.

Now you can write the code that will display the child form. First, look at the code, and then I’ll explain it:

void __fastcall
TMainForm::Chart1Click(TObject *Sender)
{
  if (ActiveChild)
    delete ActiveChild;
  TChartForm* form = new TChartForm(this);
  ActiveChild = form;
  form->Show();
  Chart1->Checked = true;
  Table1->Checked = false;
}

This method is the OnClick handler for a menu item on the main form. As you might guess, the handler displays the TChartForm child. Note that I first check to see if the ActiveChild variable is non-zero. ActiveChild will be non-zero if a child window is active. If ActiveChild is not zero, I delete the pointer associated with the variable to destroy the active child form. If I don’t first destroy the active child form, my program will continue to stack child after child on top of one another.

Next, I create an instance of the TChartForm class. I then assign the pointer returned from operator new to the ActiveChild variable. This way, the ActiveChild variable always contains a pointer to the current child form. Finally, I call the Show() method to display the child form. The last two lines of code insure that the menu displays a check mark next to the menu item representing either the table or chart view.

In order to complete the discussion of the ActiveChild variable, I have to take you back to the child form unit for a moment. Each of the child forms contains an event handler for the OnClose event that looks like this:

void __fastcall TChartForm::FormClose(
  TObject *Sender, TCloseAction &Action)
{
  MainForm->ActiveChild = 0;
  MainForm->Chart1->Checked = false;
  Action = caFree;
}

Note that when the form is destroyed, the main form’s ActiveChild variable is set to 0. I also uncheck the menu item associated with the child form, and set the Action parameter to caFree. Setting Action to caFree tells the VCL to free the memory associated with the form.

You might be wondering why the FormClose handler contains those last two lines of code. After all, I just showed you code in the main form that performs these same actions. The answer is that each of the child forms contains a button called Close that can, naturally, be used to close the form. If the form is closed using the Close button, then the memory needs to be freed and the menu item unchecked.

A few extra features

There is at least one feature of the example program that I haven’t discussed yet. That is, if the child form is larger than the current client area of the main form, the main form is resized to accommodate the child. That code is placed in the child form’s CreateParams() method. Earlier, I showed you an example of a basic CreateParams() method. I left out the code that resizes the main form because I didn’t want to introduce more complex code at that time. You can find the completed CreateParams() method for the TChartForm class in Listing B. The method only differs from that shown earlier in that it contains this code:

if (Width > MainForm->ClientWidth)
  MainForm->ClientWidth = Width;
if (Height > (MainForm->StatusBar->Top -
    MainForm->ToolBar->Height))
  MainForm->ClientHeight = Height +
  MainForm->ToolBar->Height +
  MainForm->StatusBar->Height;

This code checks to see if the child form’s width is greater than main form’s ClientWidth property. If it is, then the main form’s ClientWidth property is set to the width of the child form. The next few lines, although a bit more complex, do the same thing for the main form’s client height.

The result of this code is that the main form will always be resized to accommodate the child form being displayed.

The example program also accounts for the main form being resized. If the main form is resized, the child form must also be resized so that it continues to fill the client area of the main form. Here’s the OnResize event handler for the main form:

void __fastcall
TMainForm::FormResize(TObject *Sender)
{
  if (ActiveChild) {
    ActiveChild->Width = ClientRect.Right;
    ActiveChild->Height =
      (MainForm->StatusBar->Top - 1) -
      ActiveChild->Top;
  }
}

This code is fairly straightforward, so I don’t need to go over every line. Note, though, that I first check the value of the ActiveChild variable to be sure that it is non-zero (that is, that it points to a child form). Obviously I don’t need to do anything in the OnResize event handler if no child form is currently active. The remaining code is a variation of the code you saw in the child form’s CreateParams() method. It simply calculates the new size for the child window and sets the Width and Height properties accordingly.

Conclusion

Listing A contains the source code for the example program’s main form. Listing B shows the source code for the TChartForm unit. I don’t show the headers for these units because they don’t contain any meaningful code. I also don’t show the code for the TTableForm unit because it is identical to the code for the TChartForm unit. You can download the example program from our Web site at http://www.bridgespublishing.com.

Hosting child windows within the main form provides a clean alternative to using MDI, and also to an application that would otherwise display data to the user as modeless forms. Using child forms allows you to design your secondary windows using the form designer, and also helps you keep the code that drives the child form in one place.

Listing A: MAINU.CPP

#include <vcl.h>
#pragma hdrstop

#include "MainU.h"
#include "ChartU.h"
#include "TableU.h"

#pragma resource "*.dfm"
TMainForm *MainForm;

__fastcall
TMainForm::TMainForm(TComponent* Owner)
  : TForm(Owner)
{
  // Zero out the ActiveChild variable or it
  // will contain random data.
  ActiveChild = 0;
  // Open the main form's Table component.
  Table->Active = true;
}

void __fastcall
TMainForm::Table1Click(TObject *Sender)
{
  // If this form is already being displayed
  // then return without doing anything.
  if (Table1->Checked)
    return;
  // Delete the active child if it exists.
  if (ActiveChild) {
    delete ActiveChild;
    ActiveChild = 0;
  }
  // Create an instance of TTableForm.
  TTableForm* form = new TTableForm(this);
  // Assign the DBGrid::DataSource property of
  // the TTableForm's DBGrid to the datasource
  // on the main form.
  form->DBGrid->DataSource = DataSource;
  // Keep track of the active child.
  ActiveChild = form;
  // Show the form.
  form->Show();
  // Update the check marks on the View menu.
  Table1->Checked = true;
  Chart1->Checked = false;
}

void __fastcall
TMainForm::Chart1Click(TObject *Sender)
{
  // Essentially the same code as described
  // for the Table1Click method above.
  if (Chart1->Checked)
    return;
  if (ActiveChild)
    delete ActiveChild;
  TChartForm* form = new TChartForm(this);
  ActiveChild = form;
  form->Show();
  Chart1->Checked = true;
  Table1->Checked = false;
}

void __fastcall
TMainForm::FormResize(TObject *Sender)
{
  // If the main form is resized, resize the
  // active child to fill the client area of
  // the main form.
  if (ActiveChild) {
    ActiveChild->Width = ClientRect.Right;
    ActiveChild->Height =
      (MainForm->StatusBar->Top - 1) -
      ActiveChild->Top;
  }
}

Listing B: CHARTU.CPP

#include <vcl.h>
#pragma hdrstop

#include "ChartU.h"
#include "MainU.h"

#pragma resource "*.dfm"

TChartForm *ChartForm;

__fastcall
TChartForm::TChartForm(TComponent* Owner)
  : TForm(Owner)
{
}

void __fastcall
TChartForm::CreateParams(TCreateParams& Params)
{
  // Call the base class CreateParams method.
  TForm::CreateParams(Params);
  // Set the style to create a child window.
  Params.Style = WS_CHILD | WS_CLIPSIBLINGS;
  // Set the window's parent as the main form.
  Params.WndParent = MainForm->Handle;
  // The X position of the window is 0.
  Params.X = 0;
  // If the main form is too narrow or too short
  // to accomodate the child form, resize it.
  if (Width > MainForm->ClientWidth)
    MainForm->ClientWidth = Width;
  if (Height > (MainForm->StatusBar->Top -
      MainForm->ToolBar->Height))
    MainForm->ClientHeight = Height +
      MainForm->ToolBar->Height +
      MainForm->StatusBar->Height;
  // The Y position of the child form is just
  // below the main form's toolbar.
  Params.Y = MainForm->ToolBar->Height + 1;
  // The width of the child form is the same as
  // the main form's client width.
  Params.Width = MainForm->ClientRect.Right;
  // Calculate a height based on the bottom of
  // the toolbar, and the top of the status bar.
  Params.Height =
    (MainForm->StatusBar->Top - 1) - Params.Y;
}

void __fastcall TChartForm::FormClose(
  TObject *Sender, TCloseAction &Action)
{
  // Update the main form's ActiveChild property
  // to indicate no child is active.
  MainForm->ActiveChild = 0;
  // The child form might have been closed via
  // the Close button so update the main menu's
  // check marks, and tell the VLC to clean up
  // the memory for this form.
  MainForm->Chart1->Checked = false;
  Action = caFree;
}

void __fastcall
TChartForm::CloseBtnClick(TObject *Sender)
{
  Close();
}