Delphi makes it easy to develop MDI applications, even without using the MDI Application template available in Delphi (see the Applications page of the File ® New ® Other dialog box). You only need to build at least two forms, one with the FormStyle property set to fsMDIForm and the other with the same property set to fsMDIChild. Set these two properties in a simple program and run it, and you'll see the two forms nested in the typical MDI style.
Generally, however, the child form is not created at startup, and you need to provide a way to create one or more child windows. You can do so by adding a menu with a New menu item and writing the following code:
var ChildForm: TChildForm; begin ChildForm := TChildForm.Create (Application); ChildForm.Show;
Another important feature to add is a Window pull-down menu, which you use as the value of the form's WindowMenu property. This pull-down menu will automatically list all the available child windows. (Of course, you can choose any other name for the pull-down menu, but Window is the standard.)
To make this program work properly, you can add a number to the title of any child window when it is created:
procedure TMainForm.New1Click(Sender: TObject); var ChildForm: TChildForm; begin WindowMenu := Window1; Inc (Counter); ChildForm := TChildForm.Create (Self); ChildForm.Caption := ChildForm.Caption + ' ' + IntToStr (Counter); ChildForm.Show; end;
You can also open child windows, minimize or maximize each of them, close them, and use the Window pull-down menu to navigate among them. Now suppose you want to close some of these child windows, to unclutter the client area of your program. Click the Close boxes in some of the child windows, and they are minimized! What is happening? Remember that when you close a window, you generally hide it from view. The closed forms in Delphi still exist, although they are not visible. In the case of child windows, hiding them won't work, because the MDI Window menu and the list of windows will still list existing child windows, even if they are hidden. For this reason, Delphi minimizes the MDI child windows when you try to close them. To solve this problem, you need to delete the child windows when they are closed by setting the Action reference parameter of the OnClose event to caFree.
Your first task is to define a better menu structure for the example. Typically the Window pull-down menu has at least three items: Cascade, Tile, and Arrange Icons. To handle the menu commands, you can use some of the predefined methods of TForm that can be used only for MDI frames:
Cascade Method Cascades the open MDI child windows. The windows overlap each other. Iconized child windows are also arranged (see ArrangeIcons).
Tile Method Tiles the open MDI child windows; the child forms are arranged so that they do not overlap. The default behavior is horizontal tiling, although if you have several child windows, they will be arranged in several columns. This default can be changed by using the TileMode property (set the value to either tbHorizontal or tbVertical).
ArrangeIcons Procedure Arranges all the iconized child windows. Open forms are not moved.
As a better alternative to calling these methods, you can place an ActionList in the form and add to it a series of predefined MDI actions. The related classes are TWindowArrange, TWindowCascade, TWindowClose, TWindowTileHorizontal, TWindowTileVertical, and TWindowMinimizeAll. The connected menu items will perform the corresponding actions and will be disabled if no child window is available. The MdiDemo example, which we'll look at next, demonstrates the use of the MDI actions, among other things.
There are some other interesting methods and properties related strictly to MDI in Delphi:
ActiveMDIChild Property A run-time, read-only property of the MDI frame form that holds the active child window. The user can change this value by selecting a new child window, or the program can change it using the Next and Previous procedures, which activate the child window following or preceding the currently active one.
ClientHandle Property Holds the Windows handle of the MDI client window, which covers the client area of the main form.
MDIChildren Property An array of child windows you can use together with the MDIChildCount property to cycle among all the child windows. This property can be useful for finding a particular child window or to operate on each of them.
Note that the internal order of the child windows is the reverse order of activation. This means the last child window selected is the active window (the first in the internal list), the second-to-last child window selected is the second, and the first child window selected is the last. This order determines how the windows are arranged on the screen. The first window in the list is above all the others, whereas the last window is below all the others, and probably hidden. You can imagine an axis (the z-axis) coming out of the screen toward you. The active window has a higher value for the z coordinate and, thus, covers other windows. For this reason, the Windows ordering schema is known as the z-order.
The Window menu can be used along with the ActionManager and the ActionMainMenuBar control hosting the menu, starting with Delphi 7. This control has a specific property, WindowMenu, that you have to set to specify the menu that is going to list the MDI child windows.
I've built an example to demonstrate most of the features of a simple MDI application. MdiDemo is a full-blown MDI text editor, because each child window hosts a Memo component and can open and save text files. The child form has a Modified property that indicates whether the text of the memo has changed (it is set to True in the handler of the memo's OnChange event). Modified is set to False in the Save and Load custom methods and is checked when the form is closed (prompting the user to save the file).
As I've mentioned, the example's main form is based on an ActionList component. The actions are available through some menu items and a toolbar, as shown in Figure 8.3. You can see the details of the ActionList in the example's source code; I'll focus on the code of the custom actions.
One of the simplest actions is the ActionFont object, which has both an OnExecute handler (which uses a FontDialog component) and an OnUpdate handler (which disables the action—and hence the associated menu item and toolbar button—when there are no child forms):
procedure TMainForm.ActionFontExecute(Sender: TObject); begin if FontDialog1.Execute then (ActiveMDIChild as TChildForm).Memo1.Font := FontDialog1.Font; end; procedure TMainForm.ActionFontUpdate(Sender: TObject); begin ActionFont.Enabled := MDIChildCount > 0; end;
The action named New creates the child form and sets a default filename. The Open action calls the ActionNewExcecute method prior to loading the file:
procedure TMainForm.ActionNewExecute(Sender: TObject); var ChildForm: TChildForm; begin Inc (Counter); ChildForm := TChildForm.Create (Self); ChildForm.Caption := LowerCase (ExtractFilePath (Application.Exename)) + 'text' + IntToStr (Counter) + '.txt'; ChildForm.Show; end; procedure TMainForm.ActionOpenExecute(Sender: TObject); begin if OpenDialog1.Execute then begin ActionNewExecute (Self); (ActiveMDIChild as TChildForm).Load (OpenDialog1.FileName); end; end;
The file loading is performed by the form's Load method. Likewise, the child form's Save method is used by the Save and Save As actions. Notice the Save action's OnUpdate handler, which enables the action only if the user has changed the memo's text:
procedure TMainForm.ActionSaveAsExecute(Sender: TObject); begin // suggest the current file name SaveDialog1.FileName := ActiveMDIChild.Caption; if SaveDialog1.Execute then begin // modify the file name and save ActiveMDIChild.Caption := SaveDialog1.FileName; (ActiveMDIChild as TChildForm).Save; end; end; procedure TMainForm.ActionSaveUpdate(Sender: TObject); begin ActionSave.Enabled := (MDIChildCount > 0) and (ActiveMDIChild as TChildForm).Modified; end; procedure TMainForm.ActionSaveExecute(Sender: TObject); begin (ActiveMDIChild as TChildForm).Save; end;