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.
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.
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
Zarko,
you the man. You made it as simple as it can get. It was a perfect fit for my project.
Thanks a bunch.