Most modern applications have multiple
The CoolBar component is a Win32 common control introduced by Internet Explorer and used by some Microsoft applications.
The ControlBar component is totally VCL based, with no dependencies on external libraries.
The Win32 CoolBar common control is basically a collection of
objects that you can activate by using the editor of the Bands property, available also in the component editor menu items or through the Object TreeView. You can customize the CoolBar component in many ways. You can set a bitmap for its background, add some bands to the
collection, and then assign an existing component or component container to each
You can place one band on each line or all of them on the same line. Each would use a part of the available surface, and it would be automatically enlarged when the
The CoolDemo example's form has a
component with four bands, two for each of the two lines. The first band includes a subset of the toolbar of the previous example, this time adding an ImageList for the highlighted images. The second has an edit box used to set the font of the text; the third has a ColorGrid component, used to choose the font color and background
The user interface of the CoolBar component is nice, and Microsoft uses it in its applications, but alternatives such as the ControlBar component offer a similar UI with no troubles attached. The Windows CoolBar control has had many different and incompatible versions, because Microsoft has released different versions of the common control library with different versions of the Internet Explorer. Some of these versions "broke" existing programs built with Delphi—a very good reason for not using it now even if it is more stable.
The ControlBar is a control container, and you build it by placing other controls inside it, as you do with a panel (there is no list of Bands in it). Every control placed in the bar gets its own dragging area or grabber (a small panel with two vertical lines, on the left of the control), even a stand-alone button:
For this reason, you should generally avoid placing specific buttons inside the ControlBar, but instead add containers with
The MdEdit2 example is another version of the demo I developed to discuss the ActionList component earlier in this chapter. I've grouped the buttons into three toolbars (instead of a single one) and left the two combo boxes as stand-alone controls. All these components are inside a ControlBar so a user can arrange them at runtime, as you can see in Figure 6.12.
Figure 6.12: The MdEdit2 example at run time, while a user is rearranging the toolbars in the control bar
The following snippet of the DFM listing of the MdEdit2 example shows how the various toolbars and controls are embedded in the ControlBar component:
object ControlBar1: TControlBar Align = alTop AutoSize = True ShowHint = True PopupMenu = BarMenu object ToolBarFile: TToolBar Flat = True Images = Images Wrapable = False object ToolButton1: TToolButton Action = acNew end // more buttons... end object ToolBarEdit: TToolBar... object ToolBarFont: TToolBar... object ToolBarMenu: TToolBar AutoSize = True Flat = True Menu = MainMenu end object ComboFont: TComboBox Hint = 'Font Family' Style = csDropDownList OnClick = ComboFontClick end object ColorBox1: TColorBox... end
To obtain the standard effect, you have to disable the edges of the toolbar controls and set their style to flat. Sizing all the controls alike, so that you obtain one or two rows of elements of the same height, is not as easy as it might seem at first. Some controls have automatic sizing or various constraints. In particular, to make the combo box the same height as the toolbars, you have to tweak the type and
The ControlBar also has a shortcut menu that allows you to show or hide each of the controls currently inside it. Instead of writing code specific to this example, I've implemented a more generic (and reusable) solution. The shortcut menu, called
, is empty at design time and is
procedure TFormRichNote.FormCreate(Sender: TObject); var I: Integer; mItem: TMenuItem; begin ... // populate the control bar menu for I := 0 to ControlBar.ControlCount - 1 do begin mItem := TMenuItem.Create ( Self ); mItem.Caption := ControlBar.Controls [I].
Name; mItem.Tag := Integer (ControlBar.Controls [I]); mItem.OnClick := BarMenuClick; BarMenu.Items.Add (mItem); end;
The BarMenuClick procedure is a single event handler used by all the menu items; it uses the Tag property of the Sender menu item to refer to the element of the ControlBar associated with the item in the FormCreate method:
procedure TFormRichNote.BarMenuClick(Sender: TObject); var aCtrl: TControl; begin aCtrl := TControl ((Sender as TComponent).Tag); aCtrl.Visible := not aCtrl.Visible; end;
Finally, the OnPopup event of the menu is used to refresh the check mark of the menu items:
procedure TFormRichNote.BarMenuPopup(Sender: TObject); var I: Integer; begin // update the menu check marks for I := 0 to BarMenu.Items.Count - 1 do BarMenu.Items [I].Checked := TControl (BarMenu.Items [I].Tag).Visible; end;
If you look at the user interface of the MdEdit2 application in Figure 6.12, you'll notice that the form's menu appears inside a toolbar, hosted by the control bar, and below the application caption. All you have to do to accomplish this is set the toolbar's Menu property. You must also remove the main menu from the form's Menu property (keeping the MainMenu component on the form), to avoid having two copies of the menu on screen.
Another feature available in Delphi is support for
Delphi's docking support is connected with container controls, not only with forms. A panel, a ControlBar, and other containers (technically, any control derived from TWinControl ) can be set up as dock targets by enabling their DockSite property. You can also set the AutoSize property of these containers, so they'll show up only if they hold a control.
To be able to drag a control (an object of any
-derived class) into the dock site, simply set its
property to dkDock and its
property to dmAutomatic. This way, the control can be dragged away from its current position into a new docking container. To
All the docking and undocking operations can be tracked by using special events of the component being dragged (
) and the component that will receive the
There are also commands you can use to accomplish docking operations in code and to explore the status of a docking container. Every control can be moved to a different location using the
Moreover, if the dock container has the UseDockManager property set to True, you'll be able to use the DockManager property, which implements the IDockManager interface. This interface has many features you can use to customize the behavior of a dock container, including support for streaming its status.
As you can see from this brief description, docking support in Delphi is based on a large number of properties, events, and methods—more features than I have room to explore in detail. The
Docking support is not currently available in VisualCLX on either platform.
The MdEdit2 example, already discussed, includes docking support. The program has a second ControlBar at the bottom of the form, which accepts dragging one of the toolbars in the ControlBar at the top. Because both toolbar containers have the AutoSize property set to True, they are automatically removed when the host contains no controls. I've also set the AutoDrag and AutoDock properties of both ControlBars to True.
I had to place the bottom ControlBar inside a panel, together with the RichEdit control. Without this trick, the ControlBar, when activated and automatically resized, kept moving below the status bar, which isn't the correct behavior. In the example, the ControlBar is the only panel control aligned to the bottom, so there is no possible confusion.
To let users drag the toolbars out of the original container, you once again (as stated previously) set their DragKind property to dkDock and their DragMode property to dmAutomatic. The only two exceptions are the menu toolbar, which I decided to keep close to the typical position of a menu bar, and the ColorBox control, because unlike the combo box this component doesn't expose the DragMode and DragKind properties. (In the example's FormCreate method, you'll find code you can use to activate docking for the component, based on the "protected hack" discussed in Chapter 2.) The Fonts combo box can be dragged, but I don't want to let a user dock it in the lower control bar. To implement this constraint, I've used the control bar's OnDockOver event handler, by accepting the docking operation only for toolbars:
procedure TFormRichNote.ControlBarLowerDockOver(Sender: TObject; Source: TDragDockObject; X, Y: Integer; State: TDragState; var Accept: Boolean); begin Accept := Source.Control is TToolbar; end;
Dragging a toolbar directly from the upper control bar to the lower control bar doesn't work. The control bar doesn't resize to host the toolbar during the dragging operation, as it does when you drag the toolbar to a floating form and then to the lower control bar. This is a bug in the VCL, and it is very difficult to circumvent. As you'll see the next example, MdEdit3 works as expected even if it has the same code: It uses a different component with different VCL support code!
When you move one of the toolbars outside of any container, Delphi automatically creates a floating form; you might be tempted to set it back by closing the floating form. This doesn't work, because the floating form is removed along with the toolbar it contains. However, you can use the shortcut menu of the topmost ControlBar, also attached to the other ControlBar, to show this hidden toolbar.
The floating form created by Delphi to host undocked controls has a thin caption, the so-called toolbar caption , which by default has no text. For this reason, I've added some code to the OnEndDock event of each dockable control to set the caption of the newly created form into which the control is docked. To avoid a custom data structure for this information, I've used the text of the Hint property for these controls (which is basically not used) to provide a suitable caption:
procedure TFormRichNote.EndDock(Sender, Target: TObject; X, Y: Integer); begin if Target is TCustomForm then TCustomForm(Target).Caption := GetShortHint((Sender as TControl).Hint); end;
You can see an example of this effect in the MdEdit2 program in Figure 6.13.
Figure 6.13: The MdEdit2 example allows you to dock the toolbars (but not the menu) at the top or bottom of the form or to leave them floating.
Another extension of the example (which I haven't done) might add dock areas on the two sides of the form. The only extra effort this would require would be a routine to
Delphi provides many events and methods that give you a lot of control over docking operations, including a dock manager. To explore some of these features, try the DockTest example, a test bed for docking operations shown in Figure 6.14.
Figure 6.14: The DockTest example with three controls docked in the main form
The program handles the OnDockOver and OnDockDrop events of a dock host panel to display messages to the user, such as the number of controls currently docked:
procedure TForm1.Panel1DockDrop(Sender: TObject; Source: TDragDockObject; X, Y: Integer); begin Caption := 'Docked: ' + IntToStr (Panel1.DockClientCount); end;
In the same way, the program handles the main form's docking events. The controls have a shortcut menu you can invoke to perform docking and undocking operations in code, without the usual mouse dragging, with code like this:
procedure TForm1.menuFloatPanelClick(Sender: TObject); begin Panel2.ManualFloat (Rect (100, 100, 200, 300)); end; procedure TForm1.Floating1Click(Sender: TObject); var aCtrl: TControl; begin aCtrl := Sender as TControl; // toggle the floating status if aCtrl.Floating then aCtrl.ManualDock (Panel1, nil , alBottom) else aCtrl.ManualFloat (Rect (100, 100, 200, 300)); end;
To make the program perform properly at startup, you must dock the controls to the main panel in the initial code;
// dock memo Memo1.Dock(Panel1, Rect (0, 0, 100, 100)); Panel1.DockManager.InsertControl(Memo1, alTop, Panel1); // dock listbox ListBox1.Dock(Panel1, Rect (0, 100, 100, 100)); Panel1.DockManager.InsertControl(ListBox1, alLeft, Panel1); // dock panel2 Panel2.Dock(Panel1, Rect (100, 0, 100, 100)); Panel1.DockManager.InsertControl(Panel2, alBottom, Panel1);
The example's final feature is probably the most interesting and the most difficult to implement properly. Every time the program
procedure TForm1.FormDestroy(Sender: TObject); var FileStr: TFileStream; begin if Panel1.DockClientCount > 0 then begin FileStr := TFileStream.Create (DockFileName, fmCreate or fmOpenWrite); try Panel1.DockManager.SaveToStream (FileStr); finally FileStr.Free; end; end else // remove the file DeleteFile (DockFileName); end; procedure TForm1.FormCreate(Sender: TObject); var FileStr: TFileStream; begin // initialization code above... // reload the settings DockFileName := ExtractFilePath (Application.Exename) + 'dock.dck'; if FileExists (DockFileName) then begin FileStr := TFileStream.Create (DockFileName, fmOpenRead); try Panel1.DockManager.LoadFromStream (FileStr); finally FileStr.Free; end; end; Panel1.DockManager.ResetBounds (True); end;
This code works fine as long as all controls are initially docked. When you save the program, if one control is floating, you won't see it when you reload the settings. However, because of the initialization code inserted earlier, the control will be docked to the panel anyway, and will appear when you drag away the other controls. Needless to say, this is a messy situation. For this reason, after loading the settings, I added this further code:
for i := Panel1.DockClientCount - 1 downto do begin aCtrl := Panel1.DockClients[i]; Panel1.DockManager.GetControlBounds(aCtrl, aRect); if (aRect.Bottom - aRect.Top <= 0) then begin aCtrl.ManualFloat (aCtrl.ClientRect); Panel1.DockManager.RemoveControl(aCtrl); end; end;
The complete listing includes more commented code, which I used while developing this program; you might use it to understand what happens (which is often different from what you'd expect!). Briefly, the controls that have no size set in the dock manager (the only way I could figure out they are not docked) are shown in a floating window and are removed from the dock manager list.
If you look at the complete code for the OnCreate event handler, you'll see a lot of complex code, just to get a plain behavior. You could add more features to a docking program, but to do so you should remove other features, because some of them might conflict. Adding a custom docking form breaks features of the dock manager. Automatic alignments don't work well with the docking manager's code for restoring the status. I suggest you take this program and explore its behavior, extending it to support the type of user interface you prefer.
Remember that although docking panels make an application look nice, some users are
Another interesting feature of page controls is their specific support for docking. As you dock a new control over a PageControl, a new page is automatically added to host it, as you can easily see in the Delphi environment. To accomplish this, you set the PageControl as a dock host and activate docking for the client controls. This technique works best when you have secondary forms you want to host. Moreover, if you want to be able to move the entire PageControl into a floating window and then dock it back, you'll need a docking panel in the main form.
This is what I've done in the DockPage example, which has a main form with the following settings:
object Form1: TForm1 Caption = 'Docking Pages' object Panel1: TPanel Align = alLeft DockSite = True OnMouseDown = Panel1MouseDown object PageControl1: TPageControl ActivePage = TabSheet1 Align = alClient DockSite = True DragKind = dkDock object TabSheet1: TTabSheet Caption = 'List' object ListBox1: TListBox Align = alClient end end end end object Splitter1: TSplitter Cursor = crHSplit end object Memo1: TMemo Align = alClient end end
Notice that the Panel has the
property set to True and that the PageControl invariably
The program has two other forms with similar settings (although they host different controls):
object Form2: TForm2 Caption = 'Small Editor' DragKind = dkDock DragMode = dmAutomatic object Memo1: TMemo Align = alClient end end
You can drag these forms onto the page control to add new pages to it, with captions corresponding with the form titles. You can also undock each of these controls and even the entire PageControl. The program doesn't enable automatic dragging, which would make it
procedure TForm1.Panel1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin PageControl1.BeginDrag (False, 10); end;
You can test this behavior by running the DockPage example, and Figure 6.15
Figure 6.15: The main form of the DockPage example after a form has been docked to the page control on the left.