Delphi Programming Guide
Delphi Programmer 

Menu  Table of contents
Bookmark and Share

Part I - Foundations
  Chapter 1 Delphi 7 and Its IDE
  Chapter 2 The Delphi Programming Language
  Chapter 3 The Run-Time Library
  Chapter 4 Core Library classes
  Chapter 5 Visual Controls
  Chapter 6 Building the User Interface
  Chapter 7 Working with Forms
Part II - Delphi Object-Oriented Architectures
  Chapter 8 The Architecture of Delphi Applications
  Chapter 9 Writing Delphi Components
  Chapter 10 Libraries and Packages
  Chapter 11 Modeling and OOP Programming (with ModelMaker)
  Chapter 12 From COM to COM+
Part III - Delphi Database-Oriented Architectures
  Chapter 13 Delphi's Database Architecture
  Chapter 14 Client/Server with dbExpress
  Chapter 15 Working with ADO
  Chapter 16 Multitier DataSnap Applications
  Chapter 17 Writing Database Components
  Chapter 18 Reporting with Rave
Part IV - Delphi, the Internet, and a .NET Preview
  Chapter 19 Internet Programming: Sockets and Indy
  Chapter 20 Web Programming with WebBroker and WebSnap
  Chapter 21 Web Programming with IntraWeb
  Chapter 22 Using XML Technologies
  Chapter 23 Web Services and SOAP
  Chapter 24 The Microsoft .NET Architecture from the Delphi Perspective
  Chapter 25 Delphi for .NET Preview: The Language and the RTL
  Appendix A Extra Delphi Tools by the Author
  Appendix B Extra Delphi Tools from Other Sources
  Appendix C Free Companion Books on Delphi
  List of Figures    
  List of tables    
  List of Listings    
  List of Sidebars  

Previous Section Next Section

Multiple-Page Forms

When you need to display a lot of information and controls in a dialog box or a form, you can use multiple pages. The metaphor is that of a notebook: Using tabs, a user can select one of the possible pages. You can use two controls to build a multiple-page application in Delphi:

  • The PageControl component has tabs on one side and multiple pages (similar to panels) covering the rest of its surface. There is one page per tab, so you can simply place components on each page to obtain the proper effect both at design time and at run time.

  • The TabControl has only the tab portion but offers no pages to hold the information. In this case, you'll want to use one or more components to mimic the page change operation, or you can place different forms within the tabs to simulate the pages.

A third related class, the TabSheet, represents a single page of the PageControl. This is not a stand-alone component and is not available on the Component Palette. You create a TabSheet at design time by using the shortcut menu of the PageControl or at run time by using methods of the same control.


Delphi still includes (in the Win 3.1 tab of the Component Palette) the Notebook, TabSet, and TabbedNotebook components introduced in 32-bit versions (that is, since Delphi 2). For any other purpose, the PageControl and TabControl components, which encapsulate Win32 common controls, provide a more modern user interface. In 32-bit versions of Delphi, the TabbedNotebook component was reimplemented using the Win32 PageControl internally, to reduce the code size and update the look.

PageControls and TabSheets

As usual, instead of duplicating the Help system's list of properties and methods for the PageControl component, I've built an example that stretches the control's capabilities and allows you to change its behavior at run time. The example, called Pages, has a PageControl with three pages. The structure of the PageControl and the other key components appears in Listing 6.1.

Listing 6.1: Key Portions of the DFM of the Pages Example
Start example
object Form1: TForm1
  BorderIcons = [biSystemMenu, biMinimize]
  BorderStyle = bsSingle
  Caption = 'Pages Test'
  OnCreate = FormCreate
  object PageControl1: TPageControl
    ActivePage = TabSheet1
    Align = alClient
    HotTrack = True
    Images = ImageList1
    MultiLine = True
    object TabSheet1: TTabSheet
      Caption = 'Pages'
      object Label3: TLabel
      object ListBox1: TListBox
    object TabSheet2: TTabSheet
      Caption = 'Tabs Size'
      ImageIndex = 1
      object Label1: TLabel
      // other controls
    object TabSheet3: TTabSheet
      Caption = 'Tabs Text'
      ImageIndex = 2
      object Memo1: TMemo
        Anchors = [akLeft, akTop, akRight, akBottom]
        OnChange = Memo1Change
      object BitBtnChange: TBitBtn
        Anchors = [akTop, akRight]
        Caption = '&Change'
  object BitBtnPrevious: TBitBtn
    Anchors = [akRight, akBottom]
    Caption = '&Previous'
    OnClick = BitBtnPreviousClick
  object BitBtnNext: TBitBtn
    Anchors = [akRight, akBottom]
    Caption = '&Next'
    OnClick = BitBtnNextClick
  object ImageList1: TImageList
    Bitmap = {...}
End example

Notice that the tabs are connected to the bitmaps provided by an ImageList control and that some controls use the Anchors property to remain at a fixed distance from the right or bottom borders of the form. Even if the form doesn't support resizing (this would have been far too complex to set up with so many controls), the positions can change when the tabs are displayed on multiple lines (simply increase the length of the captions) or on the left side of the form.

Each TabSheet object has its own Caption, which is displayed as the sheet's tab. At design time, you can use the shortcut menu to create new pages and to move between pages. You can see the shortcut menu of the PageControl component in Figure 6.1, together with the first page. This page holds a list box and a small caption, and it shares two buttons with the other pages.

Click To expand
Figure 6.1: The first sheet of the PageControl of the Pages example, with its shortcut menu

If you place a component on a page, it is available only in that page. How can you have the same component (in this case, two bitmap buttons) in each page, without duplicating it? Simply place the component on the form, outside the PageControl (or before aligning it to the client area), and then move it in front of the pages, calling the control ® Bring To Front command from the form's shortcut menu. The two buttons I've placed in each page can be used to move back and forth between the pages and are an alternative to using the tabs. Here is the code associated with one of them:

procedure TForm1.BitBtnNextClick(Sender: TObject);
  PageControl1.SelectNextPage (True);

The other button calls the same procedure, passing False as its parameter to select the previous page. Notice that there is no need to check whether you are on the first or last page, because the SelectNextPage method considers the last page to be the one before the first and will move you directly between those two pages.

Now let's focus on the first page again. It has a list box, which at run time will hold the names of the tabs. If a user clicks an item in this list box, the current page changes. This is the third method available to change pages (after the tabs and the Next and Previous buttons). The list box is filled in the FormCreate method, which is associated with the OnCreate event of the form and copies the caption of each page (the Page property stores a list of TabSheet objects):

for I := 0 to PageControl1.PageCount - 1 do
  ListBox1.Items.Add (PageControl1.Pages.Caption);

When you click a list item, you can select the corresponding page:

procedure TForm1.ListBox1Click(Sender: TObject);
  PageControl1.ActivePage := PageControl1.Pages [ListBox1.ItemIndex];

The second page hosts two edit boxes (connected with two UpDown components), two check boxes, and two radio buttons, as you can see in Figure 6.2. The user can input a number (or choose it by clicking the up or down buttons with the mouse or pressing the Up or Down arrow key while the corresponding edit box has the focus), check the boxes and the radio buttons, and then click the Apply button to make the changes:

Figure 6.2: The second page of the example can be used to size and position the tabs. Here you can see the tabs on the left of the page control.
procedure TForm1.BitBtnApplyClick(Sender: TObject);
  // set tab width, height, and lines
  PageControl1.TabWidth := StrToInt (EditWidth.Text);
  PageControl1.TabHeight := StrToInt (EditHeight.Text);
  PageControl1.MultiLine := CheckBoxMultiLine.Checked;
  // show or hide the last tab
  TabSheet3.TabVisible := CheckBoxVisible.Checked;
  // set the tab position
  if RadioButton1.Checked then
    PageControl1.TabPosition := tpTop
    PageControl1.TabPosition := tpLeft;

With this code, you can change the width and height of each tab (remember that 0 means the size is computed automatically from the space taken by each string). You can choose to have either multiple lines of tabs or two small arrows to scroll the tab area, and you can move the tabs to the left side of the window. The control also lets you place tabs on the bottom or on the right, but this program doesn't allow that, because it would make the placement of the other controls quite complex.

You can also hide the last tab on the PageControl, which corresponds to the TabSheet3 component. If you hide one of the tabs by setting its TabVisible property to False, you cannot reach that tab by clicking on the Next and Previous buttons, which are based on the SelectNextPage method. Instead, you should use the FindNextPage function, which will select that page even if the tab won't become visible. A call of FindNextPage method is shown in the following new version of the Next button's OnClick event handler:

procedure TForm1.BitBtnNextClick(Sender: TObject);
  PageControl1.ActivePage := PageControl1.FindNextPage (
    PageControl1.ActivePage, True, False);

The last page has a memo component, again with the names of the pages (added in the FormCreate method). You can edit the names of the pages and click the Change button to change the text of the tabs, but only if the number of strings matches the number of tabs:

procedure TForm1.BitBtnChangeClick(Sender: TObject);
  I: Integer;
  if Memo1.Lines.Count <> PageControl1.PageCount then
    MessageDlg ('One line per tab, please', mtError, [mbOK], 0)
    for I := 0 to PageControl1.PageCount -1 do
      PageControl1.Pages [I].Caption := Memo1.Lines [I];
  BitBtnChange.Enabled := False;

Finally, the last button, Add Page, allows you to add a new tab sheet to the page control, although the program doesn't add any components to it. The (empty) tab sheet object is created using the page control as its owner, but it won't work unless you also set the PageControl property. Before doing this, however, you should make the new tab sheet visible. Here is the code:

procedure TForm1.BitBtnAddClick(Sender: TObject);
  strCaption: string;
  NewTabSheet: TTabSheet;
  strCaption := 'New Tab';
  if InputQuery ('New Tab', 'Tab Caption', strCaption) then
    // add a new empty page to the control
    NewTabSheet := TTabSheet.Create (PageControl1);
    NewTabSheet.Visible := True;
    NewTabSheet.Caption := strCaption;
    NewTabSheet.PageControl := PageControl1;
    PageControl1.ActivePage := NewTabSheet;
    // add it to both lists
    Memo1.Lines.Add (strCaption);
    ListBox1.Items.Add (strCaption);

Whenever you write a form based on a PageControl, remember that the first page displayed at run time is the page you were in before the code was compiled. For example, if you are working on the third page and then compile and run the program, it will start with that page. A common way to solve this problem is to add a line of code in the FormCreate method to set the PageControl or notebook to the first page. This way, the current page at design time doesn't determine the initial page at run time.

An Image Viewer with Owner-Draw Tabs

You can also use the TabControl and a dynamic approach, as described in the last example, in more general (and simpler) cases. Every time you need multiple pages that all have the same type of content, instead of replicating the controls in each page, you can use a TabControl and change its contents when a new tab is selected. This is what I've done in the multiple-page bitmap viewer example, called BmpViewer. The image that appears in the TabControl of this form, aligned to the whole client area, depends on the selection in the tab above it (as you can see in Figure 6.3).

Click To expand
Figure 6.3: The interface of the bitmap viewer in the BmpViewer example. Notice the owner-draw tabs.

At the beginning, the TabControl is empty. After selecting File ® Open, the user can choose various files in the File Open dialog box, and the array of strings with the names of the files (the Files property of the OpenDialog1 component) is added to the tabs (the Tabs property of TabControl1):

procedure TFormBmpViewer.Open1Click(Sender: TObject);
  if OpenDialog1.Execute then
    TabControl1.Tabs.AddStrings (OpenDialog1.Files);
    TabControl1.TabIndex := 0;
    TabControl1Change (TabControl1);

The Tabs property of a TabControl in CLX is a collection, whereas in the VCL it is simply a string list.

After you display the new tabs, you have to update the image so that it matches the first tab. To accomplish this, the program calls the method connected with the OnChange event of the TabControl, which loads the file corresponding to the current tab in the image component:

procedure TFormBmpViewer.TabControl1Change(Sender: TObject);
  Image1.Picture.LoadFromFile (TabControl1.Tabs [TabControl1.TabIndex]);

This example works, unless you select a file that doesn't contain a bitmap. The program will warn the user with a standard exception, ignore the file, and continue its execution.

The program also lets you paste the bitmap on the Clipboard (without immediately getting it, but only adding a tab that will perform the actual paste operation when selected) and copy the current bitmap to it. Clipboard support is available in Delphi via the global Clipboard object defined in the ClipBrd unit. For copying or pasting bitmaps, you can use the Assign method of the TClipboard and TBitmap classes. When you select the Edit ® Paste command in the example, a new tab named Clipboard is added to the tab set (unless it is already present). Then the number of the new tab is used to change the active tab:

procedure TFormBmpViewer.Paste1Click(Sender: TObject);
  TabNum: Integer;
  // try to locate the page
  TabNum := TabControl1.Tabs.IndexOf ('Clipboard');
  if TabNum < 0 then
    // create a new page for the Clipboard
    TabNum := TabControl1.Tabs.Add ('Clipboard');
  // go to the Clipboard page and force repaint
  TabControl1.TabIndex := TabNum;
  TabControl1Change (Self);

The Edit ® Copy operation is as simple as copying the bitmap currently in the image

Clipboard.Assign (Image1.Picture.Graphic);

To account for the possible presence of the Clipboard tab, the code of the TabControl1Change method becomes:

procedure TFormBmpViewer.TabControl1Change(Sender: TObject);
  TabText: string;
  Image1.Visible := True;
  TabText := TabControl1.Tabs [TabControl1.TabIndex];
  if TabText <> 'Clipboard' then
    // load the file indicated in the tab
    Image1.Picture.LoadFromFile (TabText)
    {if the tab is 'Clipboard' and a bitmap
    is available in the clipboard}
    if Clipboard.HasFormat (cf_Bitmap) then
      Image1.Picture.Assign (Clipboard)
      // else remove the clipboard tab
      TabControl1.Tabs.Delete (TabControl1.TabIndex);
      if TabControl1.Tabs.Count = 0 then
        Image1.Visible := False;

This program pastes the bitmap from the Clipboard each time you change the tab. The program stores only one image at a time, and it has no way to store the Clipboard bitmap. However, if the Clipboard content changes and the bitmap format is no longer available, the Clipboard tab is automatically deleted (as you can see in the previous listing). If no more tabs are left, the Image component is hidden.

An image can also be removed using either of two menu commands: Cut or Delete. Cut removes the tab after making a copy of the bitmap to the Clipboard. In practice, the Cut1Click method does nothing besides call the Copy1Click and Delete1Click methods. The Copy1Click method is responsible for copying the current image to the Clipboard; Delete1Click simply removes the current tab. Here is their code:

procedure TFormBmpViewer.Copy1Click(Sender: TObject);
  Clipboard.Assign (Image1.Picture.Graphic);
procedure TFormBmpViewer.Delete1Click(Sender: TObject);
  with TabControl1 do
    if TabIndex >= 0 then
      Tabs.Delete (TabIndex);
    if Tabs.Count = 0 then
      Image1.Visible := False;

One of the special features of the example is that the TabControl has the OwnerDraw property set to True. This means the control won't paint the tabs (which will be empty at design time) but will instead have the application do this, by calling the OnDrawTab event. In its code, the program displays the text vertically centered, using the DrawText API function. The text displayed is not the entire file path but only the filename. Then, if the text is not None, the program reads the bitmap the tab refers to and paints a small version of it in the tab itself. To accomplish this, the program uses the TabBmp object, which is of type TBitmap and is created and destroyed along with the form. The program also uses the BmpSide constant to position the bitmap and the text properly:

procedure TFormBmpViewer.TabControl1DrawTab(Control: TCustomTabControl;
  TabIndex: Integer; const Rect: TRect; Active: Boolean);
 TabText: string;
 OutRect: TRect;
  TabText := TabControl1.Tabs [TabIndex];
  OutRect := Rect;
  InflateRect (OutRect, -3, -3);
  OutRect.Left := OutRect.Left + BmpSide + 3;
  DrawText (Control.Canvas.Handle, PChar (ExtractFileName (TabText)),
    Length (ExtractFileName (TabText)), OutRect,
    dt_Left or dt_SingleLine or dt_VCenter);
  if TabText = 'Clipboard' then
    if Clipboard.HasFormat (cf_Bitmap) then
      TabBmp.Assign (Clipboard)
    TabBmp.LoadFromFile (TabText);
  OutRect.Left := OutRect.Left - BmpSide - 3;
  OutRect.Right := OutRect.Left + BmpSide;
  Control.Canvas.StretchDraw (OutRect, TabBmp);

The program has also support for printing the current bitmap, after showing a page preview form in which the user can select the proper scaling. This extra portion of the program I built for earlier editions of the book is not discussed in detail, but I've left the code in the program so you can examine it.

The User Interface of a Wizard

Just as you can use a TabControl without pages, you can also take the opposite approach and use a PageControl without tabs. Now I'll focus on the development of the user interface of a wizard. In a wizard, you direct the user through a sequence of steps, one screen at a time, and at each step you typically offer the choice of proceeding to the next step or going back to correct input entered in a previous step. Instead of tabs that can be selected in any order, wizards typically offer Next and Back buttons to navigate. This won't be a complex example; its purpose is just to give you a few guidelines. The example is called WizardUI.

The starting point is to create a series of pages in a PageControl and set the TabVisible property of each TabSheet to False (while keeping the Visible property set to True). Since Delphi 5, you can also hide the tabs at design time. In this case, you'll need to use the shortcut menu of the page control, the Object Inspector's combo box, or the Object Tree View to move to another page, instead of the tabs. But why don't you want to see the tabs at design time? You can place controls on the pages and then place extra controls in front of the pages (as I've done in the example), without seeing their relative positions change at run time. You might also want to remove the useless captions from the tabs; they take up space in memory and in the resources of the application.

In the first page, I've placed an image and a bevel control on one side and some text, a check box, and two buttons on the other side. Actually, the Next button is inside the page, and the Back button is over it (and is shared by all the pages). You can see this first page at design time in Figure 6.4. The following pages look similar, with a label, check boxes, and buttons on the right side and nothing on the left.

Click To expand
Figure 6.4: The first page of the WizardUI example at design time

When you click the Next button on the first page, the program looks at the status of the check box and decides which page is the following one. I could have written the code like this:

procedure TForm1.btnNext1Click(Sender: TObject);
  BtnBack.Enabled := True;
  if CheckInprise.Checked then
    PageControl1.ActivePage := TabSheet2
    PageControl1.ActivePage := TabSheet3;
  // move image and bevel
  Bevel1.Parent := PageControl1.ActivePage;
  Image1.Parent := PageControl1.ActivePage;

After enabling the common Back button, the program changes the active page and finally moves the graphical portion to the new page. Because this code has to be repeated for each button, I've placed it in a method after adding a couple of extra features. This is the code:

procedure TForm1.btnNext1Click(Sender: TObject);
  if CheckInprise.Checked then
    MoveTo (TabSheet2)
    MoveTo (TabSheet3);
procedure TForm1.MoveTo(TabSheet: TTabSheet);
  // add the last page to the list
  BackPages.Add (PageControl1.ActivePage);
  BtnBack.Enabled := True;
  // change page
  PageControl1.ActivePage := TabSheet;
  // move image and bevel
  Bevel1.Parent := PageControl1.ActivePage;
  Image1.Parent := PageControl1.ActivePage;

Besides the code I've already explained, the MoveTo method adds the last page (the one before the page change) to a list of visited pages, which behaves like a stack. The BackPages object of the TList class is created as the program starts, and the last page is always added to the end. When the user clicks the Back button, which is not dependent on the page, the program extracts the last page from the list, deletes its entry, and moves to that page:

procedure TForm1.btnBackClick(Sender: TObject);
  LastPage: TTabSheet;
  // get the last page and jump to it
  LastPage := TTabSheet (BackPages [BackPages.Count - 1]);
  PageControl1.ActivePage := LastPage;
  // delete the last page from the list
  BackPages.Delete (BackPages.Count - 1);
  // eventually disable the back button
  BtnBack.Enabled := not (BackPages.Count = 0);
  // move image and bevel
  Bevel1.Parent := PageControl1.ActivePage;
  Image1.Parent := PageControl1.ActivePage;

With this code, the user can move back several pages until the list is empty, at which point you disable the Back button. You need to deal with a complication: While moving from a particular page, you know which pages are "next" and "previous," but you don't know which page you we came from, because there can be multiple paths to reach a page. Only by keeping track of the movements with a list can you reliably go back.

The rest of the program code, which simply shows some website addresses, is very simple. The good news is that you can reuse the navigational structure of this example in your own programs and modify only the graphical portion and the content of the pages. Because most of the programs' labels show HTTP addresses, a user can click a label to open the default browser showing that page. You accomplish this by extracting the HTTP address from the label and calling the ShellExecute function:

procedure TForm1.LabelLinkClick(Sender: TObject);
  Caption, StrUrl: string;
  Caption := (Sender as TLabel).Caption;
  StrUrl := Copy (Caption, Pos ('http://', Caption), 1000);
  ShellExecute (Handle, 'open', PChar (StrUrl), '', '', sw_Show);

This method is hooked to the OnClick event of many labels on the form, which have been turned into links by setting the Cursor to a hand. This is one of the labels:

object Label2: TLabel
  Cursor = crHandPoint
  Caption = 'Main site:'
  OnClick = LabelLinkClick

Previous Section Next Section



|  .  |

Copyright © 2004-2016 "Delphi Sources". Delphi Programming Guide
     Twitter     Facebook