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

Let's Get Docked

by Ray Konopka
July/August 1999, Vol. 10, No. 2

Delphi's docking machinery is both broad and deep, but unfortunately the documentation is neither. Here are the high points, and some docking wisdom born of hard-won experience.

Well, I'm finally gonna do it. If you are a regular reader of this column, you know that for the past few issues, I've been avoiding the topic of docking support in Delphi 4. Well, I have no more excuses, and in this article, we will take a closer look at adding docking support to our Delphi applications. However, before we do that, I want to revisit a topic I covered earlier this year-initializing custom action classes.

Custom Action Resources

Back in the March/April 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, and at that time I did not have an explanation for this. Without constructors I had no idea how the action properties were getting initialized. I suggested that the Action List Editor could be initializing property values, but followed that with, "this would be a really bad design." Since writing that article I have found out how action properties are initialized-you're not going to believe this.

You may recall from the earlier article that before you can use a custom action class in an action list, the new action class must be registered with Delphi, which is accomplished by calling the RegisterActions procedure. This procedure is defined in the ActnList unit and looks like the following:

procedure RegisterActions( const CategoryName: string;
                           const AClasses: array of TBasicActionClasses;
                           Resource: TComponentClass );
                  

You may also recall that the Delphi documentation is very unclear regarding the purpose of the Resource parameter. In the example that I presented I simply passed TListBox using the logic that the action classes I created were associated with list boxes.

From the declaration above, the Resource parameter is a reference to a TComponent descendant. It turns out that the default Action Editor associates this resource class with the action classes passed in the AClasses parameter. When the editor creates a new instance of one of these action classes at design-time, the resource class is used to initialize the action's properties. How this happens is interesting to say the least.

According to Delphi R&D, the Resource parameter should normally refer to a TDataModule descendant (yes, a data module) that contains a TActionList component, which in turn should contain sample instances of each action specified in the AClasses parameter. The properties of each sample action are then set to the action's default or initial values. For example, Figure 1 shows the data module and action list used to initialize the standard actions that are defined in the StdActns unit. The EditCopy1 action is currently selected in the Action Editor and the Object Inspector thus shows the initial values that have been specified for the TEditCopy action class.

Figure 1: Using a DataModule to initialize custom action classes.

When a developer uses the Action Editor to create an instance of your custom action class, the editor checks the list of registered actions to determine the resource class. If one is found, an instance of the resource class is created. Recall that the resource class is actually a data module. As a result, the default values for the sample actions defined in the data module are stored in the data module's associated DFM file. By creating an instance of the data module, the default property values are read in from the DFM file.

With the data module resource created in the background, the Action Editor then searches the data module for an action that matches the action class that is being created. For example, if the developer instructs the Action Editor to create a new TEditCopy standard action, the editor searches for the first action of type TEditCopy, which from Figure 1 we can see is EditCopy1. If a matching action is found, the property values from the default action are copied to the newly created action.

Certainly an interesting way to initialize property values, isn't it? Before you make your decision consider the following. In order to create the sample actions on the data module in which to specify the default values, the action classes must already be registered in Delphi. Therefore, before you can create your resource class, you must first register your custom action passing nil as the Resource parameter to RegisterActions. After defining the data module resource, you can then unregister your custom action and then re-register it, but this time specifying your new data module descendant in the Resource parameter.

Frankly, I just don't see the benefit in all of this. In my March/April column, I achieved the same functionality by simply overriding the constructor of my custom actions. Not only are the extra steps described above not necessary, it makes much more sense to me to initialize actions in a constructor. After all, actions are components, and it is standard practice to initialize components in their constructors.

So, which approach should you use to initialize your custom actions? I'm still waiting to hear a compelling reason for using a data module resource. Until I do I will continue to initialize my custom action classes in a constructor.

Dockable Toolbars

By far the most common type of docking found in applications these days is toolbar docking. Furthermore, there are two ways of docking toolbars and fortunately for us Delphi supports both of them.

The first way is to allow toolbars to be repositioned within a general toolbar area. Delphi supports this type of docking with the new TControlBar component located on the Additional palette page. For example, drop a TControlBar onto your form and set its Align property to alTop. Plus, you will usually want to make sure the AutoSize property of the control bar is set to True. This causes the component to automatically resize itself so that it remains as small as possible while still encompassing all controls dropped onto it.

Next, we need something to drag around. You can drop any control onto a ControlBar, but typically you will drop one or more toolbars. Switch to the Win32 tab and click on the Toolbar component and then click on the control bar. This will create a toolbar that has a small grabber bar on its side. The grabber bar allows you to drag the toolbar anywhere within the control bar's bounds. To remove the groove that appears at the top of the toolbar, simply change the toolbar's EdgeBorders property to an empty set.

The second way of docking toolbars involves dragging a toolbar off of the control bar. This results in the toolbar being displayed in a floating window. In order to support this type of dragging and docking, you will need to change the DragMode property of the desired toolbar to dmAutomatic and the DragKind property to dkDock.

When the toolbar is dragged off of the control bar and displayed in a floating window, the title bar of the window displays the contents of the toolbar's Caption property. As a result, you should change this property to reflect a more meaningful name.

Dockable Menus (Revisited)

In the May/June issue, after postponing full coverage of docking for yet another issue, I did provide a small tip regarding the construction of dockable menus like those used in the Delphi 4 IDE. To briefly recap, the menu in the Delphi 4 main window is really just a TToolbar component that is configured in a particular way. Although it is possible to manually configure the toolbar so that it mimics a menu, the easiest way to accomplish this is to download the TMenuBar component from http://www.borland.com/devsupport/delphi/downloads/index.asp. It is listed toward the end of the Delphi 4 list of downloads.

After downloading and installing this component into Delphi 4, it is very easy to create docking menus. Unfortunately, the component does not come with any documentation and although easy to use, there are several issues that you need to be aware of.

Since TMenuBar is just a custom version of TToolbar, we create dockable menus using the same technique described above for creating dockable toolbars. Next, drop a TMainMenu component onto the form and populate the menu items. When you are finished, you will notice that the form displays the menu as a normal menu. To eliminate this, simply clear the Menu property of the form. Next, set the Menu property of the menu bar component to reference the main menu. Now the menu bar component displays each submenu as a button.

When using the TMenuBar component, you will not be able to select any of the menu items at design-time from the menu bar component. In order to specify event handlers for the menu items, you need to use the menu editor. Also note that if you use the menu editor to make changes to the menu, such as changing a menu name or adding a new menu, the menu bar will not be updated when you close the menu editor. To update the menu bar's display, you need to clear the menu bar's Menu property and reassign it to the menu component. Figure 2 shows a sample form, which is using the TMenuBar component.

Figure 2: TMenuBar makes it easy to create docking menus.

In order for the buttons on the TMenuBar to behave like drop-down menus, the system must have version 4.72 or later of ComCtl32.dll installed. This DLL is from Microsoft and implements the Windows Common Controls of which the toolbar is a part. The TToolbar component in Delphi is simply a VCL wrapper around the common toolbar control. Without version 4.72 or later of ComCtl32.dll available, all of the menu buttons will be the same width and will appear as standard non-flat buttons. In addition, older versions of the DLL do not convert ampersands in the caption into access characters. By the way, located in the \Info\Updates directory on the Delphi 4 CD is a simple setup program that installs the correct version of the ComCtl32.dll.

There is one more additional issue with the TMenuBar component. Specifically, it does not handle merging menus of child forms in an MDI application. Hopefully, borland.com will address this issue in the near future.

Dockable Tool Windows

The Delphi 4 IDE utilizes both dockable toolbars and dockable menus. The IDE also utilizes a third style of docking. For example, by default the Code Explorer window is docked to the left side of the Code Editor and the Messages view is docked to the bottom of the Code Editor. In fact, any of the tool windows that can be displayed in Delphi can be docked to the left, bottom, or right side of the Code Editor window.

In addition, the tool windows in Delphi can be docked on top of one another. For example, select the View|Debug Windows|Breakpoints menu item and then drag the Breakpoint window to the center of the object inspector. You will notice that the docking outline rectangle is centered within the bounds of the Object Inspector. This indicates that if the tool window is docked in this position, both the Object Inspector and the Breakpoints window will be docked to the same floating form but each tool window will be located on a separate page in a page control.

At this point, I normally show a demo application that illustrates the subject matter and then spend the next several paragraphs reviewing the code. In this issue, I'm doing things a little different. First, you may have already noticed that there is no source code example associated with this article. One reason for this is because most of the changes that need to be made in order to support docking can be accomplished by simply using the Object Inspector.

However, the primary reason for not providing a sample program is because Delphi 4 already comes with a fairly good demo program (DockEx.dpr) located in the Demos\Docking directory. In fact, the demo contains some really cool docking code (with comments). The only problem is that on the surface the demo appears to be very superficial. My intention here is to highlight the important pieces.

The main form of the DockEx program contains a TCoolBar component, which is Microsoft's common control version of TControlBar. The CoolBar contains two TToolbar components used in the same manner I described above. As a result, I'll skip this part of the demo.

Along the left edge of the main form you will find two components: the LeftDockPanel and the VSplitter. The panel is a dock site for any of the tool windows that are displayed by the application. The splitter allows the user to resize the width of the panel. What's interesting is that the panel does not have its AutoSize property set to True. According to the online help, setting AutoSize to True is suppose to automatically resize the width of the panel according to controls that are docked in it. However, this approach does not work. For some reason the panel does not resize itself when a control is docked inside it.

Instead of using the AutoSize property, the demo program handles a few of the events associated with the dock site. First, when the user drags a form over the LeftDockPanel, the OnGetSiteInfo event is generated. The demo program handles this event to determine if the selected form can actually be docked onto the panel. Next, the OnDockOver event is handled allowing the demo program to alter the size of the docking rectangle. When the user releases the mouse button to drop the form on the dock site, the OnDockDrop event is generated.

The DockEx program also supports docking tool window onto tool window. For example, when you run the program, you can dock the green window on top of the purple window. When this happens, a new floating window with a page control is created and each tool window (green and purple) is placed on a separate page. It is also possible to dock the purple window next to the green window so that they both appear in the same floating window without having the page control.

The tool windows themselves are instances of TDockableForm. The most important method in this class is CMDockClient, which is responsible for creating the correct docking host when a dockable form is dropped onto another dockable form. If the client form is dropped in the center of the dock site form, then a new TTabDockHost window is created and both the client form and the dock site form are put on separate pages. If the client form is dropped at one of the edges of the dock site form then a new TConjoinDockHost window is created and both tool windows are tiled inside the new dock site.

I encourage you to take a closer look at this demo. There are lots of well-written comments that explain the process in detail. Unfortunately, I just don't have the room to do that here.

Persistence

Okay. So now we know how to add docking toolbars and windows to our applications, but what about persistence? That is, how do we maintain a user's docking selections between program runs? For example, suppose a user decides to drag a toolbar off of the control bar so that it is floating in a separate window. When the user runs the program again, the toolbar should start out floating in a window and not docked onto the control bar.

This problem can be broken up into two tasks. The first task is to record the docking state for all dockable controls in the application. At first this sounds like a lot of work, but generally you should not have that many dockable controls. You can also use your dock sites to help out. For instance, each dock site has a DockClients property along with a DockClientCount property. Using these two properties you can determine which controls are docked in a particular dock site.

At the control level, you will probably want to look at the LRDockWidth, TBDockHeight, UndockWidth, and UndockHeight properties. LRDockWidth specifies the width of the control from the last time it was docked horizontally. TBDockHeight specifies the height of the control from the last time it was docked vertically. UndockWidth and UndockHeight specify the width and height of the control from the last time it was floating, respectively.

When you program starts, you will need to scan through the settings you saved from the previous run and then create and move the controls in an appropriate manner. The most challenging aspect of this is how to dock a control at startup that was originally a floating window. This is accomplished by creating the floating window first and then using the ManualDock method to programmatically dock a control to a specific dock site.

Docking Managers

Docking support in Delphi 4 is quite powerful, indeed. However, I am not particularly fond of the grabber bars that are displayed when a client is docked as shown in Figure 3. The bars are fine for toolbars, but for forms and other controls, I would prefer to see a small caption describing the contents of the docked control.

Figure 3: Delphi's default docking manager.

Fortunately, Delphi provides the means to alter this behavior, although it is not a trivial process. The trick is to create a custom docking manager. The default docking manager, which is implemented in the TDockTree class, is responsible for displaying the grabber bars for docked controls. By creating a custom docking manager and instructing a component to use it, we can customize the appearance of docked controls. For example, Figure 4 shows a sample form utilizing the custom manager that comes with Raize Components.

Figure 4: An example of a custom docking manager.

Unfortunately, I don't have enough room to describe the details of creating your own custom docking manager. The basic technique is to create a descendant of TDockTree and override the appropriate methods. For example, to replace the grabber bars with tiny captions, you override the PaintDockFrame method.

However, creating a descendant of TDockTree is only part of the solution. You must instruct your dock site component to use your new docking manager. This can be accomplished in two ways. The first is to create an instance of your docking manager and assign it to the DockManager property of the dock site. However, this usually only works for very basic managers.

Generally, you will create a docking manager for a specific type of component. In this case, you will need to create a descendant of the component you wish to use as your dock site. You then override the dock site's CreateDockManager method and return an instance of your new custom docking manager class.

Words of Wisdom

Before wrapping up this article, I would like to offer some free advice-use docking sparingly. Just because Microsoft supports docking toolbars in their applications doesn't mean that you must support them in yours. Docking can be quite confusing to inexperienced users and can easily result in extra technical support calls. The point is that you must weigh the added flexibility docking provides against the added complexity it adds to your application. Like virtually everything in programming it all comes down to tradeoffs.

On the Drawing Board

By now you have probably heard that Delphi 5 is coming soon. Next time, we will take a look at what the latest incarnation of our favorite development tool has to offer.v

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