1. Computing

Dock And Float Forms In Delphi - No Dragging

Implementing Floating / Dockable Forms Without Dragging Or Dock Sites

By

Dockable / Floating Delphi Form

Dockable / Floating Delphi Form

The TPageControl is a really handy pick when you need to create tabbed interfaces where only one tab sheet is presented (visible) to the user at one time. Each tab of a page control can host different controls and thus allows you to create complex user interface experience.

In one of my application, a tab on the page control hosts a TWebBrowser displaying PDF (and other supported) documents. Having the "PDF View" tab active, other tabs of the page control are hidden from the user - as this is how the page control functions.

Users asked: "Can we undock this tab so that it floats and therefore to be able to activate other tabs on the page control? We would also want to dock the viewer tab back. Further, we have two monitors and would like to move the floating viewer to the second monitor.

Since when a user asks for something, we do tend to implement the functionality, the task was to implement floating / docking tab of a page control.

When undocked, the content of the tab sheet should appear as a floating window / form. When docked, it should appear as a normal tab sheet on a page control.

The tab to be undocked did not only host the web browser control - there were quite a few other controls on the tab and the whole programming logic was already implemented. The controls on the tab were "talking" to other controls on the form where the page control was. Quite a complex UI, quite a complex programming with events.

Drag and Dock - No Go :(

Ha, easy to solve was my first reaction. Delphi has support for dragging and dropping as well as for dragging and docking for years.

Trying to implement the above user requirement by leveraging Delphi's built-in drag'n'dock support was a no go. There's only one docking site (tab sheet / page control), docking a form to a page control automatically adds a tab sheet, screen refresh rate was very bad while starting the undock operation, dragging was actually not needed, and so on.

Dock As Tab Sheet + Float As Form + No Dragging

The user only wants to be able to undock / dock a tab sheet to appear as a floating window - no dragging to float/dock is needed.

All the controls and the programming logic were already implemented for the tab sheet. The only question is "how do I move all the controls from the tab sheet to a floating form without having to rewrite the programming code"?

The answer was simple: I'll only change the parent of all the controls on the tab sheet - and move them to the form.

So, to have this, I'll need to

  • Have a secondary "floating" form that will act as a host.
  • This form has no controls on it - it is empty - as it will act as a container when floating.
  • The floating form is created when firstly needed and is not visible until the "undock" operation is requested.
  • When undocking, all the controls on the tab sheet will change their Parent and will be moved to the form.
  • The form becomes visible.
  • The tab sheet is made invisible.
  • To dock, the users closes the form and, again, all control change their Parent back to the tab sheet.
  • Floating form is hidden and waits for a call to "Float;"
Sounds easy and interesting. Let's see some code (download sample application)...

Here's the floating form code:

unit floating;

interface

uses
  Windows, Messages, SysUtils, Variants, 
  Classes, Graphics, Controls, Forms, Dialogs;

type
  TFloatingForm = class(TForm)
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    fNoFloatParent : TWinControl;
    fSetFloatControl : TControl;

    fOnBeforeDock: TNotifyEvent;
    fOnAfterDock: TNotifyEvent;
    fOnBeforeFloat: TNotifyEvent;
    fOnAfterFloat: TNotifyEvent;
  public
    procedure CreateParams (var Params: TCreateParams); override;
    constructor Create(AOwner : TComponent; const noFloatParent : TWinControl; const setFloatControl : TControl); reintroduce;

    procedure Float;

    property OnBeforeDock : TNotifyEvent read fOnBeforeDock write fOnBeforeDock;
    property OnAfterDock : TNotifyEvent read fOnAfterDock write fOnAfterDock;
    property OnBeforeFloat : TNotifyEvent read fOnBeforeFloat write fOnBeforeFloat;
    property OnAfterFloat : TNotifyEvent read fOnAfterFloat write fOnAfterFloat;
  end;

implementation
{$R *.dfm}

constructor TFloatingForm.Create(AOwner: TComponent; const noFloatParent : TWinControl; const setFloatControl : TControl);
begin
  fNoFloatParent := noFloatParent;
  fSetFloatControl := setFloatControl;

  inherited Create(AOwner);
end;

procedure TFloatingForm.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  //desktop button
  Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW;
end;

procedure TFloatingForm.Float;
var
  cnt : integer;
begin
  if Visible then Exit; //already floating

  fSetFloatControl.Visible := false;

  if Assigned(fOnBeforeFloat) then fOnBeforeFloat(self);

  //"magic" :)
  for cnt := -1 + fNoFloatParent.ControlCount downto 0 do
  begin
    fNoFloatParent.Controls[cnt].Parent := self;
  end;

  Visible := true;

  if Assigned(fOnAfterFloat) then fOnAfterFloat(self);
end;

procedure TFloatingForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
// HIDE by default!
//  Action := caNone;
end;

procedure TFloatingForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
var
  cnt : integer;
begin
  if Assigned(fOnBeforeDock) then fOnBeforeDock(self);

  for cnt:= -1 + ControlCount downto 0 do
  begin
    Controls[cnt].Parent := fNoFloatParent;
  end;

  fSetFloatControl.Visible := true;

  if Assigned(fOnAfterDock) then fOnAfterDock(self);

  //form is hidden by default (Action = caHide on OnClose)
end;

end.
Ok, let's see what we have here:
  • The TFloatingForm is a secondary form in the application and it will not be created at startup.
  • The overridden CreateParams ensures the form has a task bar button.
  • The reintroduced Create constructor provides two more required parameters: the noFloatParent is the TWinControl that originally hosts the controls on the "main" form, the setFloatControl is the control we use on the "main" form to switch from docked to floating state.
  • The Float procedure does the magic. It changes the Parent property and sets it to the floating form for all the controls that are children to noFloatParent.
  • The code handling the OnCloseQuery (floating form about to close) ensures all the child controls are placed back inside their original parent. Note that the default action when you "close" a secondary form is "caHide" - therefore it is only hidden and not freed.
The main form hosts a page control and a few tab sheets. Each tab sheet has an "undock me" button. Clicking the "Float 1" button implementation is just one line of code:
procedure TMainForm.sbtFloat1Click(Sender: TObject);
begin
  Floating1.Float;
end;
"Floating1" is a property of type TFloatingForm. The property is read only and lazy instantiates the floating form:
type
  TMainForm = class(TForm)
    ....
    procedure sbtFloat1Click(Sender: TObject);
  private
    fFloating1 : TFloatingForm;
    function GetFloating1: TFloatingForm;
    property Floating1 : TFloatingForm read GetFloating1;

    procedure Floating1AfterDock(Sender : TObject);
    procedure Floating1AfterFloat(Sender : TObject);
...
And here's the Floating1 getter implementation: GetFloating1:
function TMainForm.GetFloating1: TFloatingForm;
begin
  if fFloating1 = nil then
  begin
    fFloating1 := TFloatingForm.Create(self, TabSheet1, sbtFloat1);
    
    fFloating1.OnAfterDock := Floating1AfterDock;
    fFloating1.OnAfterFloat := Floating1AfterFloat;
  end;
  result := fFloating1;
end;
The floating form gets created only when needed - i.e. the first time we want to undock TabSheet1 by clicking on the sbtFloat1 button.

The floating form is owned by the main form, and it will get destroyed when the main form is destroyed.

There are some events raised by the floating form when it it goes to the floating state and when it gets closed to mimic the dock state. Note that the floating form always floats - it is only visible when needed - when child controls to some parent (tab sheet) are "undocked" and hosted on it (the form).

procedure TMainForm.Floating1AfterFloat(Sender: TObject);
begin
  TabSheet1.TabVisible := false;
  memo1.Lines.Add('float at ' + DateTimeToStr(Now));
end;

procedure TMainForm.Floating1AfterDock(Sender: TObject);
begin
  memo1.Lines.Add('dock at ' + DateTimeToStr(Now));
  TabSheet1.TabVisible := true;
  PageControl1.ActivePage := TabSheet1;
end;
The AfterFloat ensures tab sheet tab is made hidden. AfterDock makes it visible again, and activates the tab in the page control.

Grab The Source And Enhance

In the full source code, you'll note "Floating2" - used to dock/float tab sheet 2.

Therefore, a floating form instance acting independently for each tab sheet.

Also, note that you do not need to re-write any programming logic for the controls originally hosted by the tab sheet. When you do "Memo1.Lines.Add()" you do not care if it is inside a tab sheet 1 (docked) or inside the floating form (floating).

That's it. Super easy and you can use the TFloatingForm as the skeleton for all your groups of controls on a common parent to dock / float them.

©2014 About.com. All rights reserved.