Mirror

Delphi Frames (Views: 786)


Problem/Question/Abstract:

Understanding Delphi 5's New Visual Container Class

Answer:

Delphi 5 introduces a new, visual container class that represents an important advance in rapid application development (RAD) programming. This class, TFrame, provides you with the ability to visually configure a set of one or more components, and then to easily reuse this configuration throughout your application. This capability is so powerful that Delphi 5's integrated development environment (IDE) was re-designed to make extensive use of frames.

This article begins with a general discussion of what frames are, and what benefits they provide. It continues with a demonstration of how to create frames, and how to modify the properties of objects that appear on frame instances. Next, you'll learn how to create event handlers for frames, and how to override or extend these event handlers in frame instances. This article concludes by showing you how to add frames to the Component palette and the Object Repository, and the benefits of doing so.

Overview of Frames

There are two primary benefits of frames. The first is that, under certain circumstances, frames can dramatically reduce the amount of resources that need to be stored in a project. The second, and generally more important benefit, is that frames permit you to visually create objects that can be duplicated and extended. These happen to be the same two benefits that you enjoy with visual form inheritance (VFI).

VFI permits you to create form objects that can be inherited from easily. The main limit to VFI is that you must use the form in an all-or-nothing fashion. Specifically, when you use VFI you always create an entirely new form. Frames, on the other hand, are more similar to panels in this respect. That is, a single form can contain two or more frames. Importantly, every frame maintains its relationship with the parent TFrame class, meaning that subsequent changes to the parent class are automatically inherited by the instances. Although you could achieve a similar effect using TPanel components, doing so would be a strictly code-based operation. That is, you would have to write the code to define the TPanel descendants manually. Frames, on the other hand, are designed visually, just like forms.

Frames can also be thought of as sharing some similarities with component templates (a group of one or more components that are saved to the Component palette by selecting Component | Create Component Template). However, the similarities are limited to the fact that both component templates and frames are designed visually (unlike traditional component design, which is an exclusively code-based process). The differences between component templates and frames are actually very great. As you've already learned, a frame is an instance of a defining class, and, as such, is changed when the defining class is changed. By comparison, component templates are aggregates of components. A change to a component template has no effect on objects previously created from that template.

Creating a Frame

The following steps demonstrate how to create a frame (the code for this project is available for download; see end of article for details).

Select File | New Application to create a new project.

Select File | New Frame to create a new frame. On this frame, place three labels and three DBEdits. Also place a DBNavigator and a DataSource (as shown in Figure 1). Set the captions of the labels to ID, First Name, and Last Name. Set the DataSource property of each DBEdit and the DBNavigator to DataSource1.

With this frame still selected, set its Name property to NameFrame. (More so than other objects, it's particularly important to give a frame a meaningful name.) Finally, save the frame by selecting File | Save As. In this case, save the frame using the file name NAMEFRAM.PAS.



Figure 1: A simple frame for displaying an ID number, as well as a first and last name.

That's all there is to creating a frame. The following section demonstrates how to put it to use.

Using a Frame

A frame is a component. However, its use typically differs from most other components that appear on the Component palette. The following steps demonstrate how to use a frame:

Select Form1 of the application you created in the preceding steps.

Add two group boxes to the form, one above the other. Set the caption of the first frame to Customers, and the caption of the second to Employees. Your form may look something like that shown in Figure 2.

Now add the frames. With the Standard page of the Component palette selected, click on the Frame component and drop it in the Customers frame. Delphi responds by displaying the Select frame to insert dialog box (see Figure 3).

Select NameFrame. The frame will now appear in the Customers frame. Repeat this process, this time placing the frame within the Employees frame. You may have to select each frame and correct its size, depending on how you placed it originally. When you're done, your form should look similar to that shown in Figure 4.

Continue by placing two Table components onto the form. Set the DatabaseName property of both tables to IBLocal. Set the TableName property of Table1 to CUSTOMER and the TableName property of Table2 to EMPLOYEE. Make both tables active by setting their Active properties to True.

Here's where things get interesting. Select the DataSource in the Customers frame, and set its DataSet property to Table1. Normally you can't directly select objects that appear within a component, but frames are special. You can select any of the objects that appear within a frame, and work with their properties. Next, repeat this operation by selecting the DataSource in the Employees frame and setting its DataSet property to Table2.

Finally, hook up all the DBEdits. Assign the DataField property of the three DBEdits on the Customers frame to CUST_NO, CONTACT_FIRST, and CONTACT_LAST, respectively. For the Employees frame, set the DataField properties of these same DBEdits to EMP_NO, FIRST_NAME, and LAST_NAME.

Save this project and then run it. The running project will look something like that shown in Figure 5.



Figure 2: A form ready for the placement of frames.


Figure 3: The Select frame to insert dialog box.


Figure 4: Two instances of NameFrame appear on this form.


Figure 5: The example frame project at run time.

Frames and Inheritance

Up to this point, there may seem to be little benefit to using frames. However, it's when you use the same frame in a number of different situations, and then want to change all instances, that the power of frames becomes obvious. For example, imagine you've decided to make NameFrame read-only. This can be accomplished easily by simply changing the original frame; each frame instance immediately inherits all changes.

You can demonstrate this by following these steps:

With the project created in the preceding section, press [Shift][F12] and select NameFrame from the displayed list of forms.

Set the AutoEdit property of the DataSource to False.

Next, select the DBNavigator, expand its VisibleButtons property, and set the nbInsert, nbDelete, nbEdit, nbPost, and nbCancel flags to False.

Now look at your main form. Notice that both NameFrame descendants have inherited the changes you made to the frame (see Figure 6).



Figure 6: Updating NameFrame automatically causes all instances to be updated as well.

Overriding Contained Component Properties

One of the advantages of frames (one shared with VFI) is that you can change the properties and event handlers associated with the objects inside the inherited frame. These changes override the inherited values. Specifically, subsequent changes to the overridden property in the original frame don't affect the inherited value. The following steps demonstrate this behavior:

Select the label whose caption is "ID" in the Customers frame. Using the Object Inspector, change its Caption property to Customer No:. Now select the ID label for the Employees frame and change it to Employee ID:.

Press [Shift][F12] and select NameFrame. Change the caption of this ID label to Identifier.

Return to the main form. Notice that the Caption properties of the labels haven't changed to Identifier. They still use their overridden values.

This effect is accomplished through information stored in the DFM file. Figure 7 displays a relevant part of the DFM file for this project.



Figure 7: A DFM file containing property overrides for a frame instance.

Notice that information about all components contained within the frame whose property values have been changed appear in the frame's inline section of the DFM file. However, this section only lists those values that have been changed. All other properties are assigned their values based either on the values set for the original frame (and which are stored in the frame's DFM file), or are designated as default values in the individual component's class declarations.

Contained Object Event Handlers

Objects contained within a frame may also have event handlers. Although events are simply properties of a method pointer type, they're treated differently than other types of properties when it comes to overriding the default behavior defined for the frame.

Let's begin by considering how an event handler is defined for a frame object. Consider the frame shown in Figure 8. (This code is found in the Frame2 project found in the download for this article.) This frame contains two buttons, one labeled Help and the other Done. (Of course, these captions can be overridden in descendant frames). These buttons also have OnClick event handlers, which are shown in Figure 9.


Figure 8: A frame with components that have event handlers.

procedure TTwoButtonFrame.Button1Click(Sender: TObject);
begin
  if (TComponent(Sender).Tag = 0) or
    (Application.HelpFile = '') then
    MessageBox(Application.Handle, 'Help not available',
      'Help', MB_OK)
  else
    Application.HelpContext(TComponent(Sender).Tag);
end;

procedure TTwoButtonFrame.Button2Click(Sender: TObject);
var
  AParent: TComponent;
begin
  AParent := TComponent(Sender).GetParentComponent;
  while not (AParent is TCustomForm) do
    AParent := AParent.GetParentComponent;
  TCustomForm(AParent).Close;
end;
Figure 9: The OnClick event handlers for the Help and Done buttons on our frame.

Just as the event handlers for objects on a form are published methods of that form's class, the event handlers of objects on a frame are published methods of that frame. (The code segment doesn't actually depict the fact that these methods are published. Rather, they're declared in the default visibility section of the frame's class declaration, and the default visibility is published.)

If you inspect the code associated with the Button2Click event handler, which is associated with the Done button, you'll notice that the event handlers associated with the frame introduces an interesting artifact. Specifically, Self is the frame, not the form in which the frame is contained. Consequently, it isn't possible to simply invoke the Close method from within this event handler to close the form. When an unqualified method invocation appears in code, the compiler assumes you want it to apply to Self. Because a TFrame object doesn't have a Close method, the compiler generates an error if you simply use an unqualified call to Close.

Because the frame in this example is designed to be embedded within a form, the event handler uses the GetParentComponent method of the frame to climb the containership hierarchy within which the frame is nested. Once a TCustomForm instance is found (which will either be a TForm descendant or a custom form based upon TCustomForm), that reference is used to invoke the form's Close method.

Overriding Contained Object Event Handlers

If you're familiar with event overriding in VFI, you'll recall that Delphi embeds a call to inherited from within an overridden event handler on a descendant form. You can then alter the generated code to either add additional behavior before, or following, the call to inherited, or conditionally invoke inherited, or you can omit the call altogether.

Frame descendants don't use inherited when invoking the event handler for an object embedded on the parent frame. Instead, the ancestor frame's method is called directly. For example, if you place the TwoButtonFrame frame (shown in Figure 8) onto a form and then double-click it, Delphi will generate the following code:

procedure TForm1.TwoButtonFrame1Button2Click(
  Sender: object);
begin
  TwoButtonFrame1.Button2Click(Sender);
end;

In this generated code, TwoButtonFrame1 is the frame descendant of TTwoButtonFrame (the original frame's class). Button2Click, as you saw in the earlier code segment, is the event handler for the Done button on that frame. As a result, this code invokes the original event handler, passing it the Sender that was passed to the button on the frame instance.

This means that event handling introduces another interesting feature. Specifically, in these situations, Sender is generally not a member of the Self object. Indeed, Sender is usually a member of the form object, and Self is the frame object.

Figure 10 shows an overridden event handler for a TwoButtonFrame descendant that was placed on a form. In this case, the original behavior is "commented out," so the new behavior completely replaces the originally defined behavior for the Done button.

procedure TForm1.TwoButtonFrame1Button2Click(
  Sender: TObject);
begin
  with TForm2.Create(Self) do
  begin
    ShowModal;
    Release;
  end;
  // The following is the original, auto-generated code
  //   TwoButtonFrame1.Button2Click(Sender);
end;
Figure 10: An overridden event handler for a TwoButtonFrame descendant that was placed on a form.

The caption of this button was also overridden, so it displays the text, Start. Figure 11 shows the form on which this TwoButtonFrame descendant appears.


Figure 11: This TwoButtonFrame instance overrides both the caption and the OnClick event handler.

Frames that Save Resources

The form shown in Figure 11 actually contains two frames. We've already discussed the TwoButtonFrame frame. The second frame displays the company logo, and is named LogoFrame.

LogoFrame appears on more than one form in the FramDemo project. The alternative to using a frame to display the logo is to place an Image object on each form upon which you want the logo to appear. However, the use of a frame for this purpose significantly reduces the amount of resources that must be compiled into the .EXE, and, therefore, results in a smaller executable.


The reason for this can be seen if you consider the following segment of the DFM file for the form shown in Figure 11:

inline LogoFrame1: TLogoFrame
        Left = 6
        Top = 6
        Width = 211
        Height = 182
        inherited Image1: TImage
        Width = 211
        Height = 182
        end
end

If, instead, a TImage instance had been placed onto the form, the DFM file for the form would have had to contain the entire binary representation of the logo. Figure 12 shows a segment of LogoFrame's DFM file. (Note that it shows only a tiny portion of the entire hexadecimal representation of the binary resource.) Furthermore, every form containing one of these images would have repeated this resource. When a frame is used, however, that resource is defined only once.

object LogoFrame: TLogoFrame
  Left = 0
    Top = 0
    Width = 239
    Height = 178
    TabOrder = 0
    object Image1: TImage
    Left = 0
      Top = 0
      Width = 239
      Height = 178
      Align = alClient
      Picture.Data = {
    07544269746D6170D6540000424DD654000000000000760000...
Figure 12: A segment of LogoFrame's DFM file.

Simplifying Frame Use

Within a single, small project, it's fairly easy to use the Frame component on the Standard page of the Component palette. For larger projects, however, or for situations where you want to use the same frame in multiple applications, you need something easier. Fortunately, Delphi permits you to place individual frames onto the Component palette, permitting these frames to be used easily and repeatedly without the extra steps required by the Frame component. A frame can also be placed into the Object Repository, permitting it to be copied easily. Both of these techniques are described in the following sections.

Adding a Frame to the Component Palette

By placing a particular frame onto the Component palette, you make its placement as simple as any other. By comparison, using the Frame component on the Standard page of the Component palette requires four steps and limits you to placing frames already defined within your project. To place a particular frame onto the Component palette, follow these steps:

Save your frame to disk. If you want to use this frame in multiple applications, it's highly recommended that you save the frame to a directory that won't be deleted when you update Delphi. For example, create a folder named c:\Program Files\Borland\DelphiFrames and store your frames there.

Select the frame and right-click on it. Select Add to Palette. Delphi displays the Component Template Information dialog box (see Figure 13).

Define the name of the frame component in the Component name field, the page of the Component palette on which you want the frame to appear in the Palette page field, and, if you've created a custom 24 x 24 pixel, 16-color icon for the frame, click the Change button to select this .BMP file. Click OK when you're done.



Figure 13: The Component Template Information dialog box.

Using a Frame from the Component Palette

To use a frame previously placed on the Component palette, select the page of the Component palette onto which you saved the frame, select the frame's icon, and drop it onto the form on which you want a descendant of that frame to appear. This process requires only two steps.

Adding a Frame to the Object Repository

By adding a frame to the Object Repository, you make it easy to copy it into a new project. Especially important is the ability to use the inheritance offered by the Object Repository to place an inherited frame into a new project, thereby maintaining the relationship between the frame and its ancestor. To add a frame to the Object Repository, follow these steps:

Save your frame to disk. In addition to saving this frame to Delphi's OBJREPOS directory or to a shared directory, you can also save it to the same one to which you save frames that you add to the Component palette. Saving the frame to a shared directory is especially nice if you are using a shared object repository. This permits multiple developers to share frames.

Right-click the frame and select Add To Repository. Delphi responds by displaying the Add To Repository dialog box (see Figure 14).

Fill out the Add To Repository dialog box just as you would for any template you're adding to the Object Repository. Click OK when done.



Figure 14: The Add To Repository dialog box.

Using a Frame from the Object Repository

To use a frame from the Object Repository, use the following steps:

Select File | New.

Select the page of the Object Repository to which you saved your frame template (see Figure 15).

Select the icon for the frame; then select the Inherit radio button.

Click OK to add an inherited version of the frame to your project.



Figure 15: The location of your frame template.

If you select the Copy radio button instead of the Inherit radio button, the newly added frame will be a copy of the original frame. This is useful when you want to create a new frame, but don't want to maintain a relationship between it and the original.

Conclusion

Does it make a difference whether you place a frame you want to reuse on the Component palette or the Object Repository? The answer is a strong "Yes!" In most cases, you'll want to place frames you use frequently onto the Component palette. When you place a frame from the Component palette, you're always placing an instance of the frame class. You can then easily change the properties and event handlers of this instance as described earlier in this article. By comparison, placing a frame from the Object Repository creates a new class, not an instance. This new class is either a copy of the original or a descendant, depending on which radio button you select in the Object Repository dialog box. If you want to use a frame in a project, it makes a great deal of sense to place an instance, rather than define a new class for your frame. For this purpose, saving the frame to the Component palette is the best approach.

The one situation where you might want to use the Object Repository is when you're specifically creating hierarchies of frames, where each frame descendant introduces additional objects, methods, or event handlers. Here, the inheritance offered by the Object Repository makes it easier for you to create each new descendant. However, once you've defined the frame descendants you want to use regularly, I would again suggest that you add these to the Component palette to simplify their use.


Component Download: delphi_frames.zip

<< Back to main page