In complex MDI applications, it's common to include child windows of different kinds (that is, based on different child forms). I built an example called MdiMulti to highlight some problems you may encounter with this approach. This example has two different types of child forms: the first type hosts a circle drawn in the position of the last mouse click, and the second contains a bouncing square. The main form also has a custom background, obtained by painting a tiled image in it.
The first type of child form displays a circle in the position where the user clicked one of the mouse buttons. Figure 8.4 shows an example of the output of the MdiMulti program. The program includes a Circle menu, which allows the user to change the color of the surface of the circle as well as the color and size of its border. It's interesting that to program the child form, you do not need to consider the existence of other forms or of the frame window. You simply write the code for the form, and that's all. The only special care required is for the menus of the two forms.
If you prepare a main menu for the child form, it will replace the main menu of the frame window when the child form is activated: An MDI child window cannot have a menu of its own. But the fact that a child window can't have any menus should not bother you, because this is the standard behavior of MDI applications. You can use the frame window's menu bar to display the child window's menus. Even better, you can merge the frame window's menu bar with that of the child form. For example, in this program, the child form's menu can be placed between the frame window's File and Window pull-down menus. You can accomplish this using the following GroupIndex values:
File pull-down menu, main form: 1
Circle pull-down menu, child form: 2
Window pull-down menu, main form: 3
Using these settings for the menu group indexes, the menu bar of the frame window will have either two or three pull-down menus. At startup, the menu bar has two menus. As soon as you create a child window, there are three menus; and when the last child window is closed (destroyed), the Circle pull-down menu disappears. You should spend some time testing this behavior by running the program.
The second type of child form shows a moving image. The square (a Shape component) moves around the client area of the form at fixed time intervals, using a Timer component, and bounces against the edges of the form, changing its direction. This turning process is determined by a fairly complex algorithm, which I don't have space to examine; the main point of the example is to show you how menu merging behaves when you have an MDI frame with child forms of different types. (You can study the source code to see how it works.)
Now let's integrate the two child forms into an MDI application. The File pull-down menu has two separate New menu items, which are used to create a child window of either kind. The code uses a single child window counter. As an alternative, you could use two different counters for the two kinds of child windows. The Window menu uses the predefined MDI actions.
As soon as a form of this kind is displayed on the screen, its menu bar is automatically merged with the main menu bar. When you select a child form of one of the two kinds, the menu bar changes accordingly. Once all the child windows are closed, the main form's original menu bar is reset. By using the proper menu group indexes, you let Delphi accomplish everything automatically, as you can see in Figure 8.5.
I've added a few other menu items in the main form to close every child window and show some statistics about them. The method related to the Count command scans the MDIChildren array property to count the number of child windows of each kind (using the RTTI operator is):
for I := 0 to MDIChildCount - 1 do if MDIChildren is TBounceChildForm then Inc (NBounce) else Inc (NCircle);
The example program also includes support for a background-tiled image. The bitmap is taken from an Image component and should be painted on the form in the wm_EraseBkgnd Windows message's handler. The problem is that you cannot simply connect the code to the main form, because a separate window (the MDI Client) covers its surface.
You have no corresponding Delphi form for this window, so how can you handle its messages? You have to resort to a low-level Windows programming technique known as subclassing. (In spite of the name, it has little to do with OOP inheritance.) The basic idea is that you can replace the window procedure that receives all the window messages with a new procedure you provide. You can do so by calling the SetWindowLong API function and providing the memory address of the procedure (the function pointer).
A window procedure is a function that receives all the messages for a window. Every window must have a window procedure and can have only one. Even Delphi forms have a window procedure; although it is hidden in the system, it calls the WndProc virtual function, which you can use. However, the VCL has a predefined handler for the messages, which are then forwarded to the form's message-handling methods after some preprocessing. With all this support, you need to handle window procedures explicitly only when working with non-Delphi windows, as in this case.
Unless you have a reason to change the default behavior of this system window, you can simply store the original procedure and call it to obtain default processing. The two function pointers referring to the two procedures (old and new) are stored in two local fields on the form:
private OldWinProc, NewWinProc: Pointer; procedure NewWinProcedure (var Msg: TMessage);
The form also has a method you'll use as a new window procedure; the code will be used to paint on the background of the window. Because this is a method and not a plain window procedure, the program has to call the MakeObjectInstance method to add a prefix to the method and let the system use it as if it were a function. All this description is summarized by just two complex statements:
procedure TMainForm.FormCreate(Sender: TObject); begin NewWinProc := MakeObjectInstance (NewWinProcedure); OldWinProc := Pointer (SetWindowLong (ClientHandle, gwl_WndProc, Cardinal (NewWinProc))); OutCanvas := TCanvas.Create; end;
The window procedure you install calls the default procedure. Then, if the message is wm_EraseBkgnd and the image is not empty, you draw it on the screen many times using the Draw method of a temporary canvas. This canvas object is created when the program starts (see the previous code) and connected to the handle passed as wParam parameter by the message. With this approach, you don't have to create a new TCanvas object for every background painting operation requested, thus saving a little time in the frequent operation. Here is the code, which produces the output already seen in Figure 8.5:
procedure TMainForm.NewWinProcedure (var Msg: TMessage); var BmpWidth, BmpHeight: Integer; I, J: Integer; begin // default processing first Msg.Result := CallWindowProc (OldWinProc, ClientHandle, Msg.Msg, Msg.wParam, Msg.lParam); // handle background repaint if Msg.Msg = wm_EraseBkgnd then begin BmpWidth := MainForm.Image1.Width; BmpHeight := MainForm.Image1.Height; if (BmpWidth <> 0) and (BmpHeight <> 0) then begin OutCanvas.Handle := Msg.wParam; for I := 0 to MainForm.ClientWidth div BmpWidth do for J := 0 to MainForm.ClientHeight div BmpHeight do OutCanvas.Draw (I * BmpWidth, J * BmpHeight, MainForm.Image1.Picture.Graphic); end; end; end;