Home DevTools Library Delphi by Design Articles  
Site Map
Search
About Us
 
 
 

Delphi Frames

by Ray Konopka
November/December 1999, Vol. 10, No. 4

Grouping Delphi UI elements together in true, object-oriented containers has been tried several ways as Delphi has evolved. With Delphi 5, we now have the concept of component frames--and this time Ray thinks they've got it right.

So, have you upgraded to Delphi 5 yet? No? Well you should. It has many more features and capabilities than Delphi 3 & 4 and it does not suffer from the stability problems that plagued Delphi 4 after its initial release. In short, I highly recommend upgrading to Delphi 5.

In this article we are going to take a much closer look at the new Frames technology introduced in Delphi 5. In my Delphi 5 Preview article from the last issue, I briefly mentioned frames in the VCL Enhancements section. Specifically, Delphi 5 introduces a new type of control called a frame that can be nested within a form or another frame. The technique is similar to visual form inheritance, but because frames can be embedded within a form, they are often viewed as a way of creating a compound component visually. Also, because frames are true objects, they are vastly superior to component templates. For example, if you make a change to a frame, the change is propagated to all instances of the frame, which is not true for component templates.

On the surface, frames are certainly attractive and appear quite powerful and flexible. However, this power and flexibility is not free. In this article, we will discover just what we can and can't do with frames, but before we do, I want to revisit a topic that I have discussed in recent articles.

Action Resources Revisited

Way back in the March/April 1999 issue, I described how to create your own custom action classes. In the process, I commented on how the standard actions that come with Delphi have no constructors, but still get initialized. In the July/August 1999 issue I described how Delphi initializes custom actions using a registration resource, which as you may recall is typically a custom data module.

At that time, I did not have an explanation as to why this approach was taken, when simply overriding the custom action's constructors appears to work quite well. Since then, I have investigated a little further into this and discovered why Borland R&D took the approach they did for initializing custom actions. It all has to do with images.

Specifically, take a look at Figure 1, which shows the RkListActionResource data module being edited in Delphi 5. Note that the Action List Editor is also open and shows the actions contained in ActionList1. More importantly, notice how each action has an associated image. This is reflected in the ImageIndex property for each custom action.

Figure 1: Creating a Custom Action Resource.

After a lot of testing, I discovered a significant advantage of the resource approach versus overriding the constructor. In particular, when you add a custom action to an action list in your application and that action list is connected to an image list, the image associated with the action is automatically copied into the application specific image list. In addition, because the application's image list may already have images on it, the Action List Editor is smart enough to alter the newly created action's ImageIndex property to match the index of the image in the application's image list. Very cool, indeed.

But be warned there is a sequence that must be followed. In order to get the image associated with an action into your application's image list; the image list must be assigned to the action list before you add any custom actions to it.

By the way, the online help for Delphi 4.03 (and Delphi 5) has been updated to describe the Resource parameter of RegisterActions, but unfortunately it does not describe the significance of using an image list component inside the resource along with the action list.

So, after this enlightenment, should you adopt the new resource approach for all your custom actions? Well, it depends. If you want to associate an image with your custom action, then yes, by all means, use a custom resource to initialize the image to be used. However, if your action does not need an associated image, then I recommend simply overriding the custom action's constructor and initialize the appropriate properties-it's much easier and does not require a two-step registration process.

Introducing Frames

So, what exactly are frames? Technically, a frame is any class that descends from the TFrame base class. A more practical definition is that a frame is a specialized component container that can be dropped onto forms, components, and even other frames. Another way of looking at a frame is Visual Form Inheritance (VFI) applied to a panel. However, the best way to learn about frames is to see an example.

Figure 2 shows a form that will ultimately be used to capture information for an invoice. Also in the figure is a new frame, Frame1. This frame was created by selecting the File|New Frame menu item. After creating a new frame, I recommend renaming it to something more meaningful. In this example, I renamed the frame to AddressFrame. Next, you place components on the frame just as you would with a form. You can even write event handlers for the components on the frame. Figure 3 shows the completed address frame.

Figure 2: Frames look slightly different from forms at design-time.

Figure 3: Components are placed on a frame just like a form.

We are now ready to create an instance of our new address frame in the group boxes of the main invoice form. This is accomplished by clicking on the Frames icon located on the Standard page of the component palette followed by clicking on the form or component that will be the parent of the frame instance. This action results in the dialog box shown in Figure 4 being displayed. From here you can select which frame you would like to insert.

Figure 4: Inserting a frame-also called creating a frame instance.

At this point I should clarify some of the terminology that is used when talking about frames. First, the term frame is typically used to refer to the base class definition and its associated DFM file that describes the visual characteristics of the components that are on the frame. The process of inserting a frame somewhere is a bit misleading. In fact, the process of inserting a frame is equivalent to creating an instance of a frame. Another term that you will see commonly used is inline frame. An inline frame is simply another term for an instance of a frame.

For example, Figure 5 shows the Delphi IDE from Figure 4 just after the OK button was pressed. Using the terminology described above, the design window with AddressFrame in the caption represents the TAddressFrame frame. Inside the Bill To group box, AddressFrame1 represents an instance of the TAddressFrame frame. AddressFrame1 can also be called an inline frame.

Figure 5: The Bill To group box contains an inline frame.

Inheritance is Key

Frames are similar to component templates in that the components that are contained within the frame instance can be customized using the Form Designer and Object Inspector. However, as noted at the beginning of this article, frames are much more powerful than templates.

I like to describe component templates as specialized IDE macros. That is, when you create a component template you are essentially instructing the IDE to record each of the selected components, their properties, and their event handlers. The recorded template is stored in the Delphi.dct file located in the Delphi Bin directory and an icon is created on the component palette. This icon is used to play back the recorded macro.

What happens if you decide to change the template macro? In this case, all future playbacks of the template will include the change. However, playbacks that took place before the change are not altered. In fact, there is absolutely no connection at all between the components that are part of a template and the template itself.

This is the biggest difference between component templates and frames. Unlike templates, frames are implemented as true objects. As a result, frames take advantage of inheritance to propagate changes to all instances of the frame. As an example, consider Figure 6, which shows the same example described in Figure 5, but this time the edit field used for the State field has been changed to a combo box. Notice how both instances of the frame on the form automatically reflect the change.

Figure 6: Frames support visual inheritance.

Customizing an Inline Frame

As noted above, inline frames can be customized using the Form Designer and the Object Inspector. In fact, you can customize any component's properties within the frame instance. For example, Figure 6 shows how the Name label's Caption has been customized in each instance. Any changes made in an inline frame essentially override the default values defined in the frame. To cancel your customizations and restore the default, simply right-click on the specific property or the component and select the Revert to Inherited menu item.

In addition to customizing properties, you can also customize event handlers. For example, suppose that our address frame defines an OnChange event handler for the EdtName component. If we then double-click on the EdtName component on the AddressFrame1 instance, the following event handler is created:

procedure TForm1.AddressFrame1EdtNameChange(Sender: TObject);
begin
  AddressFrame1.EdtNameChange(Sender);
end;
                  

Notice how Delphi automatically adds a call to the default event handler defined in the TAddressFrame class. However, this only works when the event handler is already defined in the frame class. For example, suppose you create a custom event handler for an event that is not handled by the frame class. If you were to go back and create an event handler in the base class for the same event, you would not get the default event handler code inserted into your previously defined event handler. You have to add this code manually.

Multi-Page Dialogs

It is quite common these days for dialogs to use a page control to partition information-the Delphi IDE uses several. Creating these dialogs is also very easy thanks to the TPageControl component. However, for complex dialog boxes, it is often necessary to have more than one developer working on it. This becomes quite a challenge when all of the controls are dropped directly onto the tab sheets of the page control.

A better approach is to break up the multi-page dialog into separate pieces. In previous versions of Delphi, this is accomplished by creating a separate form for each page of the dialog box. Each page form has its BorderStyle property set to bsNone and in the main dialog form, all you need to do is to create an instance of each page form and set its Parent property to the appropriate tab sheet. Note, after constructing the page form, you need to set its Visible property to True to get it to appear, and you will also need to set the Left and Top properties to position the embedded form correctly.

Frames offer another way of handling complex multi-page dialogs. Instead of creating a separate form for each page, you create a separate frame. This lets multi-developers work on the page frames. In addition, because frames can be dropped onto a form at design-time, it is possible to see all of the pages in the main dialog at design-time.

However, if you want to delay creating the controls that are on a particular page until the user selects that page, then you will have to create the frame dynamically at runtime in the same way described above with the form approach. In this case, there is not much benefit of using frames over separate forms. In fact, the separate form approach does offer some benefits over frames as described in the Limitation section later in this article.

Frames and Compound Components

On the surface, a frame looks a lot like a compound component. (A compound component is a component that contains one or more embedded components.) In fact, frames and compound components do share many of the same features. However, there are some distinct differences between the two approaches.

For example, the embedded components that are in a (correctly implemented) compound component cannot be selected at design-time. As a result, it is not possible to manipulate the embedded control directly. This does not mean that the embedded control cannot be modified, it simply means that the compound component must surface additional properties to manipulate the control. The point is that with a compound component, you are only allowed to change what the component author provides access to. The benefit of this approach is consistency.

With frames, however, a developer is allowed to change any property for any control that is contained within the inline frame. This includes layout, dimensions, anything. The frame approach is certainly more flexible, but if you are trying to enforce consistency this flexibility is counter-productive. It would be nice if it were possible to prevent a user from modifying the components in an inline frame, but it's not-maybe for Delphi 6.

Another difference between the two approaches is the way in which the two are distributed. Compound components are placed inside a package and can be distributed to any other developer who can then install the package and see the component on the palette. Frames, on the other hand, are shared among developers by either using the Object Repository or by copying the *.pas and *.dfm files associated with the frame unit.

Yes, it is possible to add a frame to the component palette by selecting the Add to Palette menu item that appears when right-clicking on a frame. However, the frame is added to the palette in the same way a component template is added to the palette. The icon that is created on the palette simply provides an easy way to create an inline instance of the frame.

Limitations

As flexible as frames are there are several limitations of the technology that you should be aware of. First, unlike a descendant form, you cannot add components to an inline frame at design-time. However, you can add a component to a frame at runtime. Second, you cannot delete a component from an inline frame at design-time. Of course, you can free a component in a frame at runtime, but be warned that if the base frame code tries to access the component you destroyed, you will get an access violation.

Another way in which frames differ from forms is that TFrame does not define an OnCreate event or an OnDestroy event. The reason why these events are not available is quite complicated but suffice it to say that the issue has been thoroughly investigated by Borland R&D. In short, the problem is one of timing-there is no clear and consistent point at which these events can be generated for a frame.

Fortunately, there is way to workaround this limitation. The trick is to remember that a TFrame is just a class, and as such, it has a constructor and destructor that we can override. Of course it's not as easy as writing an event handler, but it does work.

Moving on, suppose you want to create a custom property or event for your new frame. As an example, consider the address frame that we've been using. Suppose we wanted to create a property that would rearrange the layout of the controls on the frame so that the labels appear either to the left of the corresponding data-entry control or on top of them.

Defining the actual property is fairly simple. However, the new property will not appear in the Object Inspector even if it is declared as a published property. Why this happens is the same reason custom properties for forms do not appear in the Object Inspector. To get the properties to show up you need to create a custom module and register it with Delphi. Unfortunately, even though this capability has been around since Delphi 3, there is still no easy way to create and register a custom module in Delphi.

Component Writer Guidelines

For you component writers out there, there are a few things that you need to be aware of with respect to the new frame technology. First, as stated earlier, frames are implemented in much the same way as Visual Form Inheritance (VFI). It is not exactly the same, but from a component writer's perspective it is the same technology. Therefore, in order for your custom component to be used in a frame, it must support being used with VFI. That is, it must at least specify the csInheritable style in its ControlStyle property. Most components should have no trouble supporting VFI and frames. However, if your component performs any custom streaming, then you will most certainly want to thoroughly test your component with both VFI and frames.

The new frame technology also impacts custom property and component editors. For instance, a design editor must NOT be allowed to create new components when the corresponding component is being edited in an inline frame. Here's an example, create a new application. Next, create a new frame. Drop a page control on Frame2 and set Align to alClient. Now select New Page from the context menu to create a new tab sheet. Next, create an inline instance of Frame2 onto Form1. Next, right-click on the instance of the frame on Form1 and notice how the New Page menu item is disabled. If you invoked the popup menu from the page control, then the Delete Page is also disabled. However, if you invoke the menu from the tab sheet, then the Delphi Page is enabled. However, if you try to delete the page, Delphi does not let you.

So, how do we know when to show the menu items? Fortunately, a new method has been added to the TComponentEditor class. The IsInInlined method returns True if the component is in an inline frame.

Another issue that custom design editors need to be aware of is that Designer.Form is not necessarily the same as Designer.Root. The Designer property is defined in both the TPropertyEditor and TComponentEditor classes and provides an interface reference to the current form designer. For forms, Designer.Form and Designer.Root are indeed the same, but for frames and data modules, they are not. As a result, it is crucial for custom editors that create new components to use Designer.Root as the owner of the new components. For the same reasons, the common technique of using GetParentForm to retrieve a reference to the owner will not work for a component dropped onto a frame.

On the Drawing Board

So, what do I have planned for next time? Well, actually I haven't quite decided yet. However, it is pretty safe to assume that I will cover another new technology in Delphi 5.

By the way, the Palette menu item that you see in the screen shots is not part of Delphi 5. This is a custom expert that I have written to make it easier to switch to different pages on the component palette. Please see the Raize Software web site (http://www.raize.com/Tools/Delphi/PaletteMenu.asp) for details.v

Copyright © 1999 The Coriolis Group, Inc. All rights reserved.