WM_DISPLAYCHANGE: there is no predefined record.)
- If no pre-defined record is available, we can use TMessage, a generic record defined in the
Messages unit.
The Message-handling Procedure
Writing the message-handling procedure is not much different to writing any other procedure.
Decide on a name, create the parameter list, followed by the keyword message with the message
constant you want to intercept.
When coding handlers for Windows API messages it is common practice to use the message
name as the procedure name, omitting the underscore and using the "camel case" convention
(upper case for the first character of each separate word in the name). So our procedure
will be called WMDisplayChange.
For the parameter list I use a variable name of Msg to reduce typing. Place the procedure in the
private section of the form's declaration:
Here is our procedure definition, in the implementation portion of the form:
Within the procedure we get the new video resolution by accessing the members of our Msg parameter:
ShowMessage(InToStr(Msg.LParamLo) + 'x' + IntToStr(Msg.LParamHi)) ;
Controlling Messages Trapped by Delphi
Some messages which are sent to your form are exposed in its properties or events but do not get
handled automatically. For example, when a form is resized, Delphi does do some processing but
does not handle repositioning of controls. Instead we are given an event called FormResize which
you can use to handle repositioning of controls yourself.
To demonstrate, create a new form and place a TPanel aligned to the form's bottom. Next, place
four buttons on the panel, select all four and align their tops. From the Object Inspector click
on the event tab, then double click the event called OnResize and enter this handling code:
When the form is displayed, each button on Panel1 is horizontally aligned so that they are spread
evenly across the panel. The logic used to center buttons was taken from Dialogs unit for
CreateMessageDialog.
Life Cycle of a Form
Now that we have examined the two basic types of messages and events, let's take a look at the
events in a form's life cycle.
| Action |
Event |
| Create |
OnCreate |
| Show |
OnShow |
| Paint |
OnPaint |
| Activate |
OnActivate |
| ReSize |
OnResize |
| Paint |
OnPaint |
| Close query |
OnCloseQuery |
| Close |
OnClose |
| Deactivate |
OnDeactivate |
| Hide |
OnHide |
| Destroy |
OnDestroy |
Mining the API for Messages
If you didn't know about messaging through API calls, you might think that the exposed events are
the only ones available. By searching through Win32 help's index on WM_ (as described earlier)
you can find out about other events and messages which are available.
You still have a problem to find out the order in which a form receives these messages. One trick
to discover the order of messages is to override WinProc:
Directly after the WM_MOVE event has fired, we get a message called WM_CREATE. Within this event
you can stop the creation of the form! Programmers are constantly asking, How can I stop a form
from being created? By returning -1 rather than 0 for the result of WM_CREATE you would halt the
creation of a form.
// This example allows form creation
procedure TForm4.WMCreate(var Message: TWMCreate);
begin
inherited;
Message.Result := 0;
end;
// This example halts form creation
procedure TForm4.WMCreate(var Message: TWMCreate);
begin
inherited;
Message.Result := -1;
end;There is no cleanup required: Delphi handles this for you. You still should wrap the Show method
in a try/except block:
Once WM_CREATE message has finished, the form's OnCreate event handler is fired, if you have
assigned one. WM_MOVE is sent several more times, followed by the FormShow event which is
triggered when the form's visible property is set to True.
Next comes the WM_WINDOWPOSCHANGING event. Much can be done here, like dynamically deciding
whether the user can resize or move the form. The message WM_GETMINMAXINFO allows you to control
resizing of a form, as well as its minimum and maximum dimensions. In Delphi 4, this
event/message is surfaced in TForm as a property called Constraints. If you are using two
versions of Delphi, e.g. 3 and 4, then use the following technique (conditional compiling) to
work with WM_GETMINMAXINFO:As Delphi matures, more comes to the surface from the Windows API.
If you were to port an earlier version of Delphi to version 4 without conditional compiling,
you would get undesirable results.
Hooking into TScreen and TApplication Events
Staying closer to the surface, did you know about TScreen? Within this class there is an event
called OnActiveFormChange which you might find useful. It is fired whenever the current active
form loses focus, and another form gains focus. To try it out, place the following code in the
OnCreate event of the main form:Place the following code in the public section of the form's
declaration:
procedure WhenChangingForms(Sender: TObject) ;
Finally add this code into the implementation section of the main form:
Hooking into this event allows you to be notified when the active form is changing and to handle
whatever processing needs to be done.
Another place many programmers do not think to look for messages/events is in the application
class, TApplication. For example, TScreen has the event handler OnActiveFormChange, while
TApplication has OnActivate and its twin, TApplication.OnDeactivate. Both can be used to
determine when your application gains or loses focus from/to another application.
For example, this code sends your application to the task bar when it loses focus:
WndProc and Context-Sensitive Help
Let's dig down into TApplication class and examine the procedure WndProc. WndProc has a case
statement which checks for specific messages sent to your application, as well as messages sent
by your application to itself.
I would like to focus on CM_INVOKEHELP which is defined in the Control unit. On receiving this
message Delphi executes a function called InvokeHelp. If your application has a help file
assigned to it, this is where help gets called. If you are new to writing help, hooking into
this event can help in debugging if some components fail to work correctly when the user presses
F1 for context sensitive help.
Another reason to tie into this event is to get a visual indication of the components' help
identifiers while writing help files. We will use Application.OnHelp to hook into what is going
on under the covers when help is invoked:
In the form's OnCreate event handler we can assign a function to Application.OnHelp, which can
do several things. In this example, we write a string to the form caption indicating the type of
command issued and the context help identifier. In destroying the form we must de-assign our
function to avoid an access violation.
Tips for Shutting Down an Application
As you can see there are many messages and events underlying the normal everyday things you work
with. There are enough topics for a book on the subject, but we will finish here with some tips
about several messages that are sent when shutting down an application.
If you need to do some processing before terminating your application, the message called
WM_ENDSESSION can be trapped. Suppose you would like your application to be started the next
time Windows starts up? Use the following logic:
Of course you would need to insert the proper logic to limit this code so that it executes only
when you want it to.
Is the session closing?
Use this code to detect if the current Windows session is closing:
We can hook into the WM_QUERYENDSESSION message, to stop Windows from shutting down:
When the user attempts to restart or close Windows, the application using this code will ask if
Windows is allowed to terminate. If the user responds by clicking the No button, Windows will
not shut down. If the Yes button is clicked, Windows can proceed to shut down, unless another
application prevents it.
Do yourself a favor and explore the source code which comes with Delphi. There is a wealth
of hidden treasures there for you to discover.
Demo Projects for Part III
There are three demo projects. The table below briefly describes the demos of Windows messaging
by project, then unit. Many of the units have more going on than is described here, so check out
the source code.
Project 1
This is the main project. Project 2 must be created before running Project 1.
| Unit 1 |
Description |
| WM_CREATE |
Records the event in a ListBox |
| WM_WINDOWPOSCHANGING |
Shows how to prevent resizing or repositioning the form. In this demo, there is a checkbox to enable/disable sizing and positioning |
| WM_ENTERSIZEMOVE |
Records the event in a ListBox |
| WM_EXITSIZEMOVE |
Records the event in a ListBox |
| WM_DISPLAYCHANGE |
This event is triggered when you change the video resolution, or start the change and back out. |
| WM_MOVE |
If there are three forms (form1,2 and 3) visible, and a move is detected the logic keeps two child forms hugged to the right side of the main form. Refer to comments within the WM_MOVE procedure. |
| WM_QUERYENDSESSION |
This gets triggered when the user attempts to shutdown windows. The code within the procedure allows the user to allow Windows to shutdown or to prevent the shutdown. Even if the user prevents the shutdown, another application can block the shutdown by doing similar logic. |
| WM_ENDSESSION |
The WM_ENDSESSION message is sent to an application after Windows processes the results of the WM_QUERYENDSESSION message. The WM_ENDSESSION message informs the application whether the Windows session is ending. |
| |
|
| Unit 2 |
|
| WM_CREATE |
Records the event in a ListBox |
| WM_SYSCOMMAND |
Simple demo of how to prevent the Windows screen saver from kicking in. |
| |
|
| Unit 3 |
|
| WM_CREATE |
|
| |
|
| Unit 4 |
|
| WM_CREATE |
Records the event in a ListBox |
| |
|
| Unit 5 |
|
| WndProc |
Shows a simple example of overriding a form's WndProc. Some messages which are sent to the form are displayed in a ListBox. If we were to record all messages then the ListBox would fill up in no time. Example of the logic used in this procedure, there is exception handling to ensure we do not cause a GPF. |
Project 2
Although this project is part of project 1, it can be executed by itself.
Important notes:
- There is code in the project file and in the main unit (unit6) which hides the application
- from the task-list (Ctrl+Alt+Del) dialog and from the taskbar. The only method to return to
- the application is via Alt+Tab. There is a child form which, when shown, causes two items in
- the task-list to show up.
- There is code which simulates a shadow on the main form when requesting to close the demo
| Unit 6 |
Description |
| WM_NCHITTEST |
Shows how to prevent a user from moving a form by dragging the mouse over the forms caption. Also shows how to move the form by holding and dragging the mouse in the client area of the form |
| WM_NCMOUSEMOVE |
Shows various messages being sent to the form in response to mouse positioning. |
| Unit 7 |
|
| WM_SYSCOMMAND |
Shows how to prevent the user from minimizing or maximizing a form. For example, if certain conditions are met at runtime, allow minimizing and maximizing; otherwise disallow these actions. Alternatively, simply use a dialog-style form. |
Project 3
A stand-alone project not directly attached to Project 1 or 2.
| Unit 8 |
Description |
| WM_TIMECHANGE |
Demonstrates how to detect when the system time changes. To make it simple, you simply press a button and the time is incremented by one millisecond, we broadcast the action to all top level windows and in return we are notified ourselves. |
type
TForm1 = class(TForm)
private
procedure WMQueryEndSession(var Msg: TWMQueryEndSession) ;
message WM_QUERYENDSESSION ;
public
end;
...
...
procedure TForm1.WMQueryEndSession(var Msg: TWMQueryEndSession);
const
ABORT_WINDOWS_SHUTDOWN = 0 ;
CONTINUE_WINDOWS_SHUTDOWN = 1 ;
begin
inherited ;
if MessageDlg( 'Allow ShutDown', mtConfirmation,[mbYes,mbNo],0) = mrYes then
Msg.Result := CONTINUE_WINDOWS_SHUTDOWN
else
Msg.Result := ABORT_WINDOWS_SHUTDOWN ;
end;
procedure TForm1.WMEndSession(var Msg: TWMEndSession) ;
begin
inherited ;
if Msg.EndSession then
begin
Close ;
end ;
Msg.Result := 0 ;
End ;
type
TForm1 = class(TForm)
private
{ Private declarations }
WMEndSession(var Msg:TWMEndSession) ;
message WM_ENDSESSION ;
public
{ Public declarations }
end;
procedure TForm1.WMEndSession(var Msg: TWMEndSession) ;
const
xKey = 'Software\Microsoft\Windows\CurrentVersion\RunOnce' ;
var
Reg:TRegistry ;
begin
Reg := TRegistry.Create ;
try
{ HKEY_LOCAL_MACHINE will work on all user logins
Run will work more than once }
Reg.RootKey := HKEY_CURRENT_USER ;
if Reg.OpenKey(xKey,True) then
Reg.WriteString('kg_MyApp','"' + ParamStr(0) + '"') ;
finally
Reg.CloseKey ;
Reg.Free ;
inherited
end ;
end ;
type
TfrmMain = class(TForm)
...
private
function TestHelp(Command: word; Data: LongInt; var CallHelp: boolean):boolean ;
public
end;
...
TfrmMain.FormCreate(Sender: TObject);
begin
Application.OnHelp := TestHelp;
end;
//
function TfrmMain.TestHelp(Command: word; Data: LongInt;
var CallHelp: boolean):boolean;
begin
Caption := Format('Command %d HelpContext %d',[Command,Data]);
CallHelp := False;
end;
//
procedure TfrmMain.FormDestroy(Sender: TObject);
begin
Application.OnHelp := nil;
end;
procedure TForm1.FormCreate(Sender: TObject) ;
Begin
Applicaton.OnDeactivate := DeActivate ;
End ;
//
Procedure TForm1.DeActivate(Sender: TObject ) ;
Begin
Application.Minimize ;
End ;
procedure TForm1.WhenChangingForms(Sender: TObject);
begin
if Screen.ActiveForm <> nil then
ShowMessage('WhenChangingForms fired' + #13
+ Screen.ActiveForm.Name)
end;
Screen.OnActiveFormChange := WhenChangingForms ;
type
TfrmMain = class(TForm)
private
{$IFNDEF VER120}
procedure WMGetMinMaxInfo(var Message: TWMGetMinMaxInfo);
message WM_GETMINMAXINFO ;
{$ENDIF}
public
{ Public declarations }
end;
implementation
{$IFNDEF VER120}
procedure TfrmMain.WMGetMinMaxInfo(var Message: TWMGetMinMaxInfo) ;
begin
with Message.MinMaxInfo^ do
ptMinTrackSize := Point(330,213);
Message.Result := 0;
inherited;
end;
{$ENDIF}
try
f.ShowModal;
finally
f.Release;
end ;
type
TForm5 = class(TForm)
Panel1: TPanel;
cmdClose: TButton;
ListBox1: TListBox;
chkDisableMessages: TCheckBox;
procedure cmdCloseClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure chkDisableMessagesClick(Sender: TObject);
private
function MsgToStr( WM : word ) : String;
protected
bFlag:boolean ;
procedure WndProc(var Msg: TMessage); override ;
// TCustomForm
{ Public declarations }
end;
function TForm5.MsgToStr( WM : word ) : String;
begin
case WM of
WM_ACTIVATE: result:= 'WM_ACTIVATE';
WM_CANCELMODE: result:= 'WM_CANCELMODE ';
WM_CLOSE: result:= 'WM_CLOSE';
WM_COMMAND: result:= 'WM_COMMAND';
WM_CREATE: result:= 'WM_CREATE';
WM_ENDSESSION: result:= 'WM_ENDSESSION';
...
else result := '' ;
{ I don't care for this demo }
end;
end;
procedure TForm5.WndProc(var Msg: TMessage);
var
S:String;
begin
if Msg.Msg = wm_Close then bFlag := False;
if (bFlag) and (Self.Showing) then
begin
S := MsgToStr(Msg.Msg);
if Length(S) >0 then
ListBox1.Items.Add(S);
end;
inherited WndProc(Msg);
end;
procedure TForm5.FormCreate(Sender: TObject);
begin
ListBox1.Clear;
bFlag := True;
chkDisableMessages.Checked := bFlag;
end;Note: The case statement in MsgToStr in the sample project contains many more checks on messages
than I have shown in this example.WndProc passes the type of message it received to a function
called MsgToStr, which returns a string representation of the numeric value sent to it.
Be aware of traps, though. For example, attempting to access the ListBox when closing the
form would generate an access violation. Using this logic we will not see all the messages
sent to us!
Hand-coding as an Option
Another choice is to hand-code each procedure for each event. Make each procedure add text to a
ListBox so we can see the order of events i.e. WM_SIZE, WM_CREATE etc.
The declarations for these events go into the private section of the forms interface:
Next, we need to implement the procedures:
Running the code with a ListBox as our "eye", we would see something like this:
02:37:25 Pm WM_Move
02:37:25 Pm WMCreate
02:37:25 Pm FormCreate
02:37:25 Pm WM_Move
02:37:25 Pm WM_Move
02:37:25 Pm FormShow
02:37:25 Pm WMPosChange
02:37:25 Pm WMPosChange
02:37:25 Pm FormActivate
02:37:26 Pm WhenChangingForms
02:37:26 Pm WMPosChange
02:37:26 Pm FormResize
02:37:26 Pm WMPosChange
02:37:26 Pm WMPosChange
02:37:26 Pm WM_Move
02:37:26 Pm FormActivate
02:37:26 Pm FormPaint
Notice the message WM_MOVE is received before the OnCreate event for the form! This is why
I have logical statements within the form's WMMove procedure. Without them, we would get
access violations because some of the variables to the forms and their components do not exist
at this time.
if Screen.FormCount = 3 then begin
if (Form2.Visible) and (Form3.Visible) then
begin
...
procedure TForm1.WMDisplayChange(var Msg: TMessage) ;
begin
ListboxAddItem(Self,IntToStr(Msg.LParamLo)
+ 'x' + IntToStr(Msg.LParamHi));
end;
procedure TForm1.WMCreate(var Message: TWMCreate);
begin
inherited;
ListboxAddItem(Self,'WMCreate');
Message.Result := 0;
end;
procedure TForm1.WMPosChange(var Msg: TWMWINDOWPOSCHANGING);
begin
if not bFlag then exit;
PWindowPos(TMessage(Msg).lParam).Flags :=
PWindowPos(TMessage(Msg).lParam).Flags or SWP_NOMOVE or SWP_NOSIZE;
ListboxAddItem(Self,'WMPosChange');
end;
procedure TForm1.WMEnterSizeMove(var Msg: TMessage);
begin
inherited ;
ListboxAddItem(Self,'WM_EnterSizeMove');
Msg.Result := 0;
end;
procedure TForm1.WMExitSizeMove(var Msg: TMessage);
begin
inherited;
ListboxAddItem(Self,'WM_ExitSizeMove');
Msg.Result := 0;
end;
procedure TForm1.WMMove(var Message: TWMMove);
begin
inherited;
ListboxAddItem(Self,'WM_Move') ;
{ If both child forms are visible we make them hug the right side
of this form. We don't want to complicate matters too much for
moving either form if one was not visible.
NOTE: WM_MOVE is executed before this form's OnCreate event and
also before the two child forms are available. Hence, there is a call
to check how many forms are available, to avoid causing a GPF
when the two child forms are not yet available. }
if Screen.FormCount = 3 then
begin
if (Form2.Visible = True) and (Form3.Visible = True) then begin
with Form2 do
begin
ListboxAddItem(Self,'Moving Form2 from WMMove');
Top := Self.Top;
Self.Left + Self.Width;
end;
with Form3 do
begin
ListboxAddItem(Self,'Moving Form3 from WMMove');
Top := Form2.Top + Form2.Height;
Left := Form2.Left;
end;
end;
end;
end;
{ This event is fired each time the active form changes.
An example would be to allow Form2 to be shown non-modally
then allow the user to attempt to show Form2 in modally.
In this case, an exception would prevent ShowModal, since the
form is already visible. To avoid this, I use a For loop which
checks to see if Form2 is visible. If so, the Command Button
which shows Form2 as a modal form is disabled. }
procedure TForm1.WhenChangingForms(Sender: TObject);
var
i, iCount: Integer ;
begin
if Screen.ActiveForm <> nil then
ListboxAddItem(Self,'WhenChangingForms');
iCount := Screen.FormCount;
for i := 0 to iCount -1 do
if Screen.Forms[i] is TForm2 then
cmdShowModalForm.Enabled := not Screen.Forms[i].Visible;
end;
private
{ Private declarations }
bFlag: boolean ;
procedure WMCreate(var Message: TWMCreate);
message WM_CREATE;
procedure WMPosChange(var Msg: TWMWINDOWPOSCHANGING);
message WM_WINDOWPOSCHANGING ;
procedure WMEnterSizeMove(var Msg: TMessage);
message WM_ENTERSIZEMOVE ;
procedure WMExitSizeMove(var Msg: TMessage);
message WM_EXITSIZEMOVE ;
procedure WMDisplayChange(var Msg: TMessage);
message WM_DISPLAYCHANGE ;
procedure WMMove(var Message: TWMMove);
message WM_MOVE ;
public
{ Public declarations }
procedure WhenChangingForms(Sender: TObject);
..
procedure TForm1.FormResize(Sender: TObject);
const
ButtonWidth = 75 ;
ButtonHeight = 25 ;
ButtonCount = 4 ;
ButtonSpacing = 5 ;
var
i, x,
ButtonGroupWidth : Integer ;
begin
ButtonGroupWidth := ButtonWidth * ButtonCount + ButtonSpacing * (ButtonCount - 1) ;
X := (Panel1.ClientWidth - ButtonGroupWidth) div 2 ;
for i := 0 To Panel1.ControlCount - 1 do
begin
if Panel1.Controls[i] is TButton then
with Panel1.Controls[i] do
SetBounds(X,Panel1.Controls[i].Top,ButtonWidth, ButtonHeight) ;
Inc(X, ButtonWidth + ButtonSpacing) ;
end ;
end;
implementation
...
procedure TForm1.WMDisplayChange(var Msg: TMessage) ;
begin
{...}
end ;
type
TForm1 = class(TForm)
private
{ Private declarations }
procedure WMDisplayChange(var Msg: TMessage) ;
Message WM_DISPLAYCHANGE ;
public
{ Public declarations }
end;