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 DraggingThe 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;"
Here's the floating form code:
Ok, let's see what we have here:
Windows, Messages, SysUtils, Variants,
Classes, Graphics, Controls, Forms, Dialogs;
TFloatingForm = class(TForm)
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
fNoFloatParent : TWinControl;
fSetFloatControl : TControl;
procedure CreateParams (var Params: TCreateParams); override;
constructor Create(AOwner : TComponent; const noFloatParent : TWinControl; const setFloatControl : TControl); reintroduce;
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;
constructor TFloatingForm.Create(AOwner: TComponent; const noFloatParent : TWinControl; const setFloatControl : TControl);
fNoFloatParent := noFloatParent;
fSetFloatControl := setFloatControl;
procedure TFloatingForm.CreateParams(var Params: TCreateParams);
Params.ExStyle := Params.ExStyle or WS_EX_APPWINDOW;
cnt : integer;
if Visible then Exit; //already floating
fSetFloatControl.Visible := false;
if Assigned(fOnBeforeFloat) then fOnBeforeFloat(self);
for cnt := -1 + fNoFloatParent.ControlCount downto 0 do
fNoFloatParent.Controls[cnt].Parent := self;
Visible := true;
if Assigned(fOnAfterFloat) then fOnAfterFloat(self);
procedure TFloatingForm.FormClose(Sender: TObject; var Action: TCloseAction);
// HIDE by default!
// Action := caNone;
procedure TFloatingForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
cnt : integer;
if Assigned(fOnBeforeDock) then fOnBeforeDock(self);
for cnt:= -1 + ControlCount downto 0 do
Controls[cnt].Parent := fNoFloatParent;
fSetFloatControl.Visible := true;
if Assigned(fOnAfterDock) then fOnAfterDock(self);
//form is hidden by default (Action = caHide on OnClose)
- 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.
"Floating1" is a property of type TFloatingForm. The property is read only and lazy instantiates the floating form:procedure TMainForm.sbtFloat1Click(Sender: TObject); begin Floating1.Float; end;
And here's the Floating1 getter implementation: GetFloating1: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); ...
The floating form gets created only when needed - i.e. the first time we want to undock TabSheet1 by clicking on the sbtFloat1 button.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 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).
The AfterFloat ensures tab sheet tab is made hidden. AfterDock makes it visible again, and activates the tab in the page control.
procedure TMainForm.Floating1AfterFloat(Sender: TObject);
TabSheet1.TabVisible := false;
memo1.Lines.Add('float at ' + DateTimeToStr(Now));
procedure TMainForm.Floating1AfterDock(Sender: TObject);
memo1.Lines.Add('dock at ' + DateTimeToStr(Now));
TabSheet1.TabVisible := true;
PageControl1.ActivePage := TabSheet1;
Grab The Source And EnhanceIn 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.