I've mentioned the Application global object on multiple occasions, but because this chapter focuses on the structure of Delphi applications, it is time to delve into the details of this global object and its corresponding class. Application is a global object of the TApplication class, defined in the Forms unit and created in the Controls unit. The TApplication class is a component, but you cannot use it at design time. Some of its properties can be directly set in the Application page of the Project Options dialog box; others must be assigned in code.
To handle its events, Delphi includes a handy ApplicationEvents component. Besides allowing you to assign handlers at design time, the advantage of this component is that it allows for multiple handlers. If you simply place an instance of the ApplicationEvents component in two different forms, each of them can handle the same event, and both event handlers will be executed. In other words, multiple ApplicationEvents components can chain the handlers.
Some of these application-wide events, including OnActivate, OnDeactivate, OnMinimize, and OnRestore, allow you to keep track of the status of the application. Other events are forwarded to the application by the controls receiving them, as in OnActionExecute, OnActionUpdate, OnHelp, OnHint, OnShortCut, and OnShowHint. Finally, there is the OnException global exception handler we used in Chapter 2 ("The Delphi Programming Language"), the OnIdle event used for background computing, and the OnMessage event, which fires when a message is posted to any of the windows or windowed controls of the application.
Although its class inherits directly from TComponent, the Application object has a window associated with it. The application window is hidden from sight but appears on the Taskbar. This is why Delphi names the window Form1 and the corresponding Taskbar icon Project1.
The window related to the Application object—the application window—serves to keep together all the windows of an application. The fact that all the top-level forms of a program have this invisible owner window, for example, is fundamental when the application is activated. When your program's windows are behind other programs' windows, clicking one window in your application will bring all of that application's windows to the front. In other words, the unseen application window is used to connect the application's various forms. (The application window is not hidden, because that would affect its behavior; it simply has zero height and width, and therefore it is not visible.)
In Windows, the Minimize and Maximize operations are associated by default with system sounds and a visual animated effect. Applications built with Delphi produce the sound and display the visual effect by default.
When you create a new, blank application, Delphi generates code for the project file that includes the following:
begin Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end.
As you can see in this standard code, the Application object can create forms, setting the first one as the MainForm (one of the Application properties) and closing the entire application when this main form is destroyed. The program execution is enclosed in the Run method, which embeds the system loop to process system messages. This loop continues until the application's main window (the first window you created) is closed.
As we saw in the splash screen example in Chapter 7, the main form is not necessarily the first form you create, but the first one you create by calling Application.CreateForm.
The Windows message loop embedded in the Run method delivers the system messages to the proper application windows. A message loop is required by any Windows application, but you don't need to write one in Delphi because the Application object provides a default loop.
In addition to performing this main role, the Application object manages a few other interesting areas:
Hints (discussed at the end of Chapter 5, "Visual Controls")
The help system, which includes the ability to define the type of help viewer (a topic not covered in detail in this book)
Application activation, minimization, and restoration
A global exceptions handler, as discussed in the ErrorLog example of Chapter 2.
General application information, including the MainForm, executable filename and path (ExeName), Icon, and Title displayed in the Windows Taskbar and when you scan the running applications with the Alt+Tab keys
To avoid a discrepancy between the two titles, you can change the application's title at design time. In case the caption of the main form changes at runtime, you can copy it to the title of the application with this code: Application.Title := Form1.Caption.
In most applications, you don't care about the application window, apart from setting its Title and icon and handling some of its events. However, you can perform some other simple operations. Setting the ShowMainForm property to False in the project source code indicates that the main form should not be displayed at startup. Inside a program, you can use the Application object's MainForm property to access the main form.
There is no better proof that a window exists for the Application object than to display it, an in the ShowApp example. You don't need to show it—you just need to resize it and set a couple of window attributes, such as the presence of a caption and a border. You can perform these operations using Windows API functions on the window indicated by the Application object's Handle property:
procedure TForm1.Button1Click(Sender: TObject); var OldStyle: Integer; begin // add border and caption to the app window OldStyle := GetWindowLong (Application.Handle, gwl_Style); SetWindowLong (Application.Handle, gwl_Style, OldStyle or ws_ThickFrame or ws_Caption); // set the size of the app window SetWindowPos (Application.Handle, 0, 0, 0, 200, 100, swp_NoMove or swp_NoZOrder); end;
The GetWindowLong and SetWindowLong API functions access the system information related to the window. In this case, you use the gwl_Style parameter to read or write the styles of the window, which include its border, title, system menu, border icons, and so on. This code gets the current styles and adds (using an or statement) a standard border and a caption to the form.
Of course, you generally won't need to implement something like this in your programs. But knowing the application object has a window connected to it is an important aspect of understanding the default structure of Delphi applications and being able to modify it when needed.
To show how the activation of forms and applications works, I've written a self-explanatory example called ActivApp. This example has two forms. Each form has a Label component (LabelForm) used to display the form's status. The program uses text and color to indicate this status information, as the handlers of the first form's OnActivate and OnDeactivate events demonstrate:
procedure TForm1.FormActivate(Sender: TObject); begin LabelForm.Caption := 'Form2 Active'; LabelForm.Color := clRed; end; procedure TForm1.FormDeactivate(Sender: TObject); begin LabelForm.Caption := 'Form2 Not Active'; LabelForm.Color := clBtnFace; end;
The second form has a similar label and similar code.
The main form also displays the status of the entire application. It uses an ApplicationEvents component to handle the Application object's OnActivate and OnDeactivate events. These two event handlers are similar to the two listed previously; the only difference is that they modify the text and color of a second label on the form and that one of them makes a beep.
If you run this program, you'll see whether this application is active and, if so, which of its forms is active. By looking at the output (see Figure 8.1) and listening for the beep, you can understand how Delphi triggers each of the activation events. Run the program and play with it for a while to understand how it works. Later, we'll get back to other events related to the activation of forms.
We have already explored some of the properties and events of the Application object. Other interesting global information about an application is available through the Screen object, whose base class is TScreen. This object holds information about the system display (the screen size and the screen fonts) and also about the current set of forms in a running application. For example, you can display the screen size and the list of fonts by writing:
Label1.Caption := IntToStr (Screen.Width) + 'x' + IntToStr (Screen.Height); ListBox1.Items := Screen.Fonts;
TScreen also reports the number and resolution of monitors in a multimonitor system. Right now, however, I will focus on the list of forms held by the Screen object's Forms property, the top-most form indicated by the ActiveForm property, and the related OnActiveFormChange event. Note that the forms the Screen object references are the forms of the application and not those of the system.
These features are demonstrated by the Screen example, which maintains a list of the current forms in a list box. This list must be updated each time a new form is created, an existing form is destroyed, or the program's active form changes. To see how this process works, you can create secondary forms by clicking the button labeled New:
procedure TMainForm.NewButtonClick(Sender: TObject); var NewForm: TSecondForm; begin // create a new form, set its caption, and run it NewForm := TSecondForm.Create (Self); Inc (nForms); NewForm.Caption := 'Second ' + IntToStr (nForms); NewForm.Show; end;
Note that you need to disable the automatic creation of the secondary form by using the Forms page of the Project Options dialog box. One of the key portions of the program is the form's OnCreate event handler, which fills the list the first time and then connects a handler to the OnActive- FormChange event:
procedure TMainForm.FormCreate(Sender: TObject); begin FillFormsList (Self); // set the secondary form's counter to 0 nForms := 0; // set an event handler on the screen object Screen.OnActiveFormChange := FillFormsList; end;
The code used to fill the Forms list box is inside a second procedure, FillFormsList, which is also installed as an event handler for the Screen object's OnActiveFormChange event:
procedure TMainForm.FillFormsList (Sender: TObject); var I: Integer; begin // skip code in destruction phase if Assigned (FormsListBox) then begin FormsLabel.Caption := 'Forms: ' + IntToStr (Screen.FormCount); FormsListBox.Clear; // write class name and form title to the list box for I := 0 to Screen.FormCount - 1 do FormsListBox.Items.Add (Screen.Forms[I].ClassName + ' - ' + Screen.Forms[I].Caption); ActiveLabel.Caption := 'Active Form : ' + Screen.ActiveForm.Caption; end; end;
It is very important not to execute this code while the main form is being destroyed. As an alternative to testing whether the list box is set to nil, you could test the form's ComponentState for the csDestroying flag. Another approach would be to remove the OnActiveFormChange event handler before exiting the application; that is, handle the main form's OnClose event and assign nil to Screen.OnActiveFormChange.
The FillFormsList method fills the list box and sets a value for the two labels above it to show the number of forms and the name of the active form. When you click the New button, the program creates an instance of the secondary form, gives it a new title, and displays it. The Forms list box is updated automatically because of the handler installed for the OnActiveFormChange event. Figure 8.2 shows the output of this program when some secondary windows have been created.
Each secondary form has a Close button you can click to remove it. The program handles the OnClose event, setting the Action parameter to caFree, so that the form is destroyed when it is closed. This code closes the form, but it doesn't update the list of the windows properly. The system moves the focus to another window first, firing the event that updates the list, and destroys the old form only after this operation.
The first idea I had to update the windows list properly was to introduce a delay, posting a user-defined Windows message. Because the posted message is queued and not handled immediately, if you send it at the last possible moment of the secondary form's life, the main form will receive it when the other form is destroyed. The trick is to post the message in the secondary form's OnDestroy event handler. To accomplish this, you need to refer to the MainForm object by adding a uses statement in the implementation portion of this unit. I've posted a wm_User message, which is handled by a specific message method of the main form, as shown here:
public procedure ChildClosed (var Message: TMessage); message wm_User; procedure TMainForm.ChildClosed (var Message: TMessage); begin FillFormsList (Self); end;
The problem is that if you close the main window before closing the secondary forms, the main form exists, but its code can no longer be executed. To avoid another system error (an Access Violation Fault), you need to post the message only if the main form is not closing. But how do you determine whether the form is closing? One way is to add a flag to the TMainForm class and change its value when the main form is closing, so that you can test the flag from the code of the secondary window.
This is a good solution—so good that the VCL already provides similar functionality with the ComponentState property and its csDestroying flag, as mentioned earlier. Therefore, you can write the following code:
procedure TSecondForm.FormDestroy(Sender: TObject); begin if not (csDestroying in MainForm.ComponentState) then PostMessage (MainForm.Handle, wm_User, 0, 0); end;
With this code, the list box always lists all the forms in the application.
After giving this approach some thought, however, I found an alternative and much more Delphi-oriented solution. The trick is to consider that every time a component is destroyed, it tells its owner about the event by calling the Notification method defined in the TComponent class. Because the secondary forms are owned by the main form, as specified in the NewButtonClick method's code, you can override this method and simplify the code (see the Screen2 folder for this version's code):
procedure TMainForm.Notification(AComponent: TComponent; Operation: TOperation); begin inherited Notification(AComponent, Operation); if (Operation = opRemove) and Showing and (AComponent is TForm) then FillFormsList; end;
If the secondary forms were not owned by the main form, you could have used the FreeNotification method to get the secondary forms to notify the main form when they are destroyed. FreeNotification receives as parameter the component to notify when the current component is destroyed. The effect is a call to the Notification method that comes from a component other than the owned components. FreeNotification is generally used by component writers to safely connect components on different forms or data modules.
The last feature I've added to both versions of the program is simple: When you click an item in the list box, the corresponding form is activated using the BringToFront method. Nice—well, almost nice. If you click the list box when the main form is not active, the main form is activated first, and the list box is rearranged; so, you might end up selecting a different form than you were expecting. If you experiment with the program, you'll soon realize what I mean. This minor glitch in the program is an example of the risks you face when you dynamically update information and let the user work on it at the same time.