Float And Dock Controls In Delphi – No Dock Sites, No Dragging – Implementing Generic Floating Container

In one of my applications, 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.

One users asked: “Can we undock this tab so that it floats and then 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.

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 to create complex user interface designs.

So, 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 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 “stay on top” form.

Generic solution to undocking and floating parts of UI

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
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs;

type
  TFloatingForm = class(TForm)
    procedure FormCreate(Sender: TObject);
    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;

var
  FloatingForm: TFloatingForm;

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;

procedure TFloatingForm.FormCreate(Sender: TObject);
begin
  FormStyle := fsStayOnTop;
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 tab 1” button implementation is just one line of code:

procedure TMainForm.Button1Click(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)
...
  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, Button1);

    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 Button1 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.Floating1AfterDock(Sender: TObject);
begin
  memo1.Lines.Add('dock at ' + DateTimeToStr(Now));
  TabSheet1.TabVisible := true;

  PageControl1.ActivePage := TabSheet1;
end;

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

Do 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.

I’m using the same approach to “undock” part of the user interface where there’s no tab sheet nor page control – to float a panel hosting other controls.

3 thoughts on “Float And Dock Controls In Delphi – No Dock Sites, No Dragging – Implementing Generic Floating Container

  1. sasha

    Thanks for the post.
    I’m trying to use a similar idea for docking/undocking. Unfortunately, there is a deep flaw soemwhere.
    The issue is that the control windows are re-created when changing their parents.
    In particular a web browser control is re-created and performs navigation once that is done.
    This is an unacceptable behavior that I can’t seem to work around.
    In my case I use a Frame to host an Edge control and the frame itself is either docked on a TGridPanel or on a standalone form. As in the sample above, I do that by changing the Frame’s Parent. And that results in windows being re-created.

    Reply
    1. zarkogajic Post author

      Hi Sasha,

      I have something similar in my app. In OnBeforeFloat/Dock I would “save” what’s inside the Edge control and reload in OnAfterFloat/Dock. Of course, this will completely reload the page – and maybe that’s not what you can work with.

      -žarko

      Reply
  2. misterTi

    Zarko,
    you the man. You made it as simple as it can get. It was a perfect fit for my project.
    Thanks a bunch.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.