Creating a stack of embedded forms
Every modern browser has a tabbed interface. Also, many other kinds of multiple views software have this kind of interface. Why? Because it's very useful. While you are reading one page, you can rapidly check another page, and then still come back to the first one at the same point you left some seconds ago. You don't have to redo a search or redo a lot of clicks to just go back to that particular point. You simply have switched from one window to another and back to the first. I see too many business applications that are composed by a bounce of dialog windows. Every form is called with the TForm.ShowModal
method. So, the user has to navigate into your application one form at time. This is simpler to handle for the programmer, but it's less user-friendly for your customers. However, providing a switchable interface to your customer is not that difficult. In this recipe, we'll see a complete example on how to do it.
Getting ready
This recipe is a bit more complex than the previous recipes, so I'll not explain all the code but only the fundamental parts. You can find the complete code in the book's code repository (Chapter1\RECIPE05
).
Let's say we want to create a tabbed interface for our software that is used to manage product orders, sales, and invoices. All the forms must be usable at the same time without having to close the previous one. Before we begin, the following screenshot is what we want to create:
How to do it...
The project is composed by a bounce of forms. The main form has a TTabControl
component that allows switching between the active forms. All embedded forms inherit from EmbeddableForm
. The most important is the Show
method shown as follows:
procedure TEmbeddableForm.Show(AParent: TPanel); begin Parent := AParent; BorderStyle := bsNone; BorderIcons := []; Align := alClient; Show; end;
Note
Note that all the forms apart from the main form have been removed from the Auto-Create Form list (Project | Options | Forms).
All the other forms descend from the EmbeddableForm
method and are added to the TTabControl
component on the main form with a line of code similar to the following:
procedure TMainForm.MenuOrdersClick(Sender: TObject); begin AddForm(TForm1.Create(self)); end;
The MainForm AddForm
method is in charge of adding an actual instance of a form into the tabs, keeping a reference of it. The following code shows you how this is done:
//Add a form to the stack procedure TMainForm.AddForm( AEmbeddableForm: TEmbeddableForm); begin AEmbeddableForm.Show(Panel1); //each tab show the caption of the containing form and //hold the reference to it TabControl1.Tabs.AddObject( AEmbeddableForm.Caption, AEmbeddableForm); ResizeTabsWidth; ShowForm(AEmbeddableForm); end;
Other methods are in charge of bringing an already created form to the front when a user clicks on the related tab and then to close a form when the related tab is removed (check the ShowForm
and WMEmbeddedFormClose
methods).
How it works...
There is a bit of code, but the concepts are simple:
- When we need to create a new form, add it in the
TabControl1.Tabs
property. The caption of the form is the caption of the tab, and the object is the form itself. This is what theAddForm
method does with the following line:TabControl1.Tabs.AddObject( AEmbeddableForm.Caption, AEmbeddableForm);
- When a user clicks on a tab, we have to find the associated form cycling through the
TabControl1.Tabs.Objects
list and bring it to the front. - When a form asks for closing (sending a
WM_EMBEDDED_CLOSE
message), we have to set theParentWantClose
property and then call theClose
method of the correspondent form. - If a user wants to close a form by closing the correspondent tab (in the recipe code, there is a
TPopMenu
component connected to theTabControl
component, which is used to close a form with a right-click), we have to call theClose
method on the correspondent form. - Every form frees itself in the
OnClose
event handler. This is done once for all in theTEmbeddableForm.CloseForm
event handler using thecaFree
action.
There's more...
Embedding a form into another TWinControl
is not difficult and allows you to create flexible GUIs without using TPageControl
and frames. For the end user, this multitabbed GUI is probably more familiar because all the modern browsers use it, and probably your user already knows how to use a browser with different pages or screens opened. From the developer point of view, the multitabbed interface allows for much better programming patterns and practices. This technique can also be used for other scenarios where you have to embed one screen into another.
More flexible (and complex) solutions can be created involving the use of Observers, but in simple cases, this recipe's solution based on Windows Messaging is enough.
More information about the Observer design pattern can be found at http://sourcemaking.com/design_patterns/observer/delphi.
Another interesting solution (that does not rely on Windows Messaging and so is also cross platform) may be based on the System.Messaging.TMessageManager
class. More info about TMessageManager
can be found at http://docwiki.embarcadero.com/Libraries/XE6/en/System.Messaging.TMessageManager.
The code in this recipe can be used with every component that uses TStringList
to show items (TListBox
, TComboBox
, and so on) and can be adapted easily for other scenarios.
In the recipe code, you'll also find a nice way to show status messages generated by the embedded forms and a centralized way to show application hints in the status bar.