The Windows operating system uses messages to notify the application that an event occurred. The operating system generates a message every time something happens — for example, when the user clicks the Start button, types something, or moves a window — and sends that message to the appropriate window.
Messages have several details, the most important of which are the window handle, the message identifier, and two message parameters. When a message occurs, its details are written to a TMsg record and then passed to the appropriate application. The TMsg record is declared in the Windows unit, shown in Listing 20-11.
Listing 20-11: Windows message details
tagMSG = packed record hwnd: HWND; message: UINT; wParam: WPARAM; lParam: LPARAM; time: DWORD; pt: TPoint; end; TMsg = tagMSG;
The hwnd field is the handle of the window to which the message is sent. In the Windows operating system, window handles are 32-bit integer values used to uniquely identify a window. Also, in the Windows operating system, all controls, like buttons, edit boxes, and list boxes, are windows and have a handle value that uniquely identifies them.
The message field is an integer value that identifies the message. For each Windows message there is a more descriptive constant in the Messages unit. All Windows message constants start with the WM_ prefix.
Listing 20-12: Several standard Windows messages
WM_MOVE = $0003; { window has been moved } WM_ENABLE = $000A; { change window state } WM_PAINT = $000F; { repaint the window client area } WM_CLOSE = $0010; { close the window } WM_KEYDOWN = $0100; { a key is pressed } WM_LBUTTONDOWN = $0201; { the left mouse button is pressed }
The wParam and lParam fields are closely related to the message passed in the message field. Both values are used to further define the message. The time and pt values are rarely used. The time field specifies the time when the event occurred, and the pt field contains the X and Y coordinates of the mouse cursor at the time the event occurred.
The VCL encapsulates the majority of standard messages into events like OnClick and OnMouseDown, but it also gives you the ability to handle other messages. To handle a message that isn't encapsulated in a VCL event, you have to write a message method.
To create a message method, you have to create a method that accepts a single var TMessage (or a similar message-related record) parameter and you have to mark the method with the message directive followed by the constant that identifies the handled message. In VCL Forms applications, the TMessage record, rather than the TMsg record, is used when working with messages.
Now let's try to handle the WM_MOVE message that is sent to the window after it has been moved. The message method that handles the WM_MOVE message should look like this:
procedure MyMoveHandler(var Message: TMessage); message WM_MOVE;
Listing 20-13 contains the entire message method that simply displays the top and left coordinates of the main form when the form is moved (see Figure 20-5).
Figure 20-5: Handling the WM_MOVE message
Listing 20-13: The WM_MOVE message method
type TForm1 = class(TForm) Label1: TLabel; private { Private declarations } public { Public declarations } procedure WMMove(var Message: TMessage); message WM_MOVE; end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.WMMove(var Message: TMessage); begin Caption := Format('Top: %d - Left: %d', [Top, Left]); end;
The difference between VCL events and direct message handling is that we have to pass the message to the ancestor's message handler and, for some messages, notify the operating system that we've processed the message. The documentation for every message specifies what to return when we handle it.
procedure TForm1.WMMove(var Message: TMessage); begin Caption := Format('Top: %d - Left: %d', [Top, Left]); inherited; { pass message to the ancestor's message handler } end;
As said earlier, the lParam and wParam fields provide us with more information about the message. In the WM_MOVE message, lParam contains both X and Y coordinates of the window. The low-order word of the lParam value specifies the X coordinate and the high-order word specifies the Y coordinate.
Listing 20-14: Reading coordinates from the lParam value
procedure TForm1.WMMove(var Message: TMessage); begin Caption := Format('X: %d - Y: %d', [Message.LParamLo, Message.LParamHi]); inherited; end;
An even better way to handle a message is to use a record more related to the message than the generic TMessage record. The Messages unit contains a large collection of such records. Here's the record related to the WM_MOVE message:
TWMMove = packed record Msg: Cardinal; Unused: Integer; case Integer of 0: ( XPos: Smallint; YPos: Smallint); 1: ( Pos: TSmallPoint; Result: Longint); end;
You should always use these message-related records, because if nothing else, they result in cleaner code, as shown in Listing 20-15.
Listing 20-15: Using a message-related record
type TForm1 = class(TForm) private public procedure WMMove(var Message: TWMMove); message WM_MOVE; end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.WMMove(var Message: TWMMove); begin Caption := Format('X: %d - Y: %d', [Message.XPos, Message.YPos]); inherited; end;
Now that we know how to use messages, we can do something a bit more fun. For instance, we can create elastic windows that behave like the Main window and Playlist windows in Winamp and other media players. First, add another form to the project and then attach it to the main form in the WM_MOVE message handler, as shown in Listing 20-16.
Listing 20-16: Automatically moving another form in the WM_MOVE message handler
type TForm1 = class(TForm) procedure FormShow(Sender: TObject); private public procedure WMMove(var Message: TWMMove); message WM_MOVE; end; var Form1: TForm1; implementation uses Unit2; {$R *.dfm} procedure TForm1.WMMove(var Message: TWMMove); begin { we have to test if Form2 exists because the WM_MOVE message for the main form gets generated before the Form2 is created } if Assigned(Form2) then begin Form2.Top := Top + Height; Form2.Left := Left; end; inherited; end; procedure TForm1.FormShow(Sender: TObject); begin Form2.Show; end;
There is a section in Chapter 12 that describes how you can move a form that has no title bar. The implementation involved three different event handlers and a Boolean variable that was used to determine when the form should be moved. Although the code itself is not complicated, there is actually too much of it. A better way to implement this functionality is to catch and respond to the WM_NCHITTEST message.
The WM_NCHITTEST is sent to a window when a mouse button is pressed or released or when the mouse cursor is moved. The return value of this message indicates the position of the mouse cursor — whether the mouse is over the client area, the title bar, the menu, or any other window portion. The message result also determines what the operating system does with the window. So, the only thing that we have to do is trick Windows into believing that the mouse is always over the title bar by returning the HTCAPTION constant in the message result.
Listing 20-17: Enabling the user to move the form by clicking on its client area
type TForm1 = class(TForm) private public procedure WMNCHitTest(var Message: TWMNCHitTest); message WM_NCHITTEST; end; var Form1: TForm1; implementation uses Unit2; {$R *.dfm} procedure TForm1.WMNCHitTest(var Message: TWMNCHitTest); begin inherited; if Message.Result = HTCLIENT then Message.Result := HTCAPTION; end;
The result of this code is displayed in Figure 20-6.
Figure 20-6: Moving both forms by clicking in the client area of the main form
The C++ language doesn't have a message directive that can be used to identify a method as a message handler. To handle a message in C++, you have to create a message method, which will be called in response to a message, and a message map in which you list your message handling methods.
For instance, to handle the WM_MOVE message in a C++Builder VCL Forms application, first create the message method:
class TMainForm : public TForm { __published: // IDE-managed components private: // User declarations public: // User declarations void __fastcall WMMove(TWMMove &Message); }; void __fastcall TMainForm::WMMove(TWMMove &Message) { Caption = Format("X: %d - Y: %d", ARRAYOFCONST((Message.XPos, Message.YPos))); }
When you're done with the method, you need to create the message map and add your method to the message map to notify the compiler that your method is a message handler.
To create a message map, you use two macros: BEGIN_MESSAGE_MAP and END_MESSAGE_MAP(BaseClass). The BaseClass is responsible for handling all other messages that aren't handled inside the message map.
To define your method as a message handler, you have to use the VCL_MESSAGE_HANDLER macro, which requires you to pass the message you are handling, the record (structure) that holds the message data, and the name of the message handler.
Here's how a message map looks:
BEGIN_MESSAGE_MAP VCL_MESSAGE_HANDLER(Message, Structure, Method) END_MESSAGE_MAP(BaseClass)
So, to qualify the WMMove method as a message handler for the WM_MOVE message, you have to create the following message map (in the public section of the class):
class TMainForm : public TForm { __published: // IDE-managed components private: // User declarations public: // User declarations __fastcall TMainForm(TComponent* Owner); void __fastcall WMMove(TWMMove &Message); BEGIN_MESSAGE_MAP VCL_MESSAGE_HANDLER(WM_MOVE, TWMMove, WMMove); END_MESSAGE_MAP(TForm) };
If you have to pass the message to the base class, which is necessary in some cases, you cannot use the inherited reserved word. You have to call the Dispatch method of the base class:
BaseClass::Dispatch(&Message);
For instance, in order to work properly, the WM_NCHITTEST message handler needs to pass the message to the base class:
class TMainForm : public TForm { __published: // IDE-managed components private: // User declarations public: // User declarations __fastcall TMainForm(TComponent* Owner); void __fastcall WMMove(TWMMove &Message); void __fastcall WMNCHitTest(TWMNCHitTest &Message); BEGIN_MESSAGE_MAP VCL_MESSAGE_HANDLER(WM_MOVE, TWMMove, WMMove); VCL_MESSAGE_HANDLER(WM_NCHITTEST, TWMNCHitTest, WMNCHitTest); END_MESSAGE_MAP(TForm) }; void __fastcall TMainForm::WMNCHitTest(TWMNCHitTest &Message) { // first call the base class handler TForm::Dispatch(&Message); if(Message.Result == HTCLIENT) Message.Result = HTCAPTION; }
To create a custom message for use in an application, you only have to define an integer constant in the range of WM_USER to WM_USER + $7FFF (see Listing 20-18).
Listing 20-18: A custom message
uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs; const MM_CHANGECOLOR = WM_USER + 1; type TForm1 = class(TForm) private { Private declarations } public { Public declarations } end;
After you've created a custom message, write a message method that will handle the message. Since this is a custom message, you can do whatever you want with it. You can, for instance, treat the wParam message field as a color value and use the MM_CHANGECOLOR message to change the color of the form, as shown in Listing 20-19.
Listing 20-19: Handling the custom message
type TForm1 = class(TForm) private public procedure MMChangeColor(var Message: TMessage); message MM_CHANGECOLOR; end; procedure TForm1.MMChangeColor(var Message: TMessage); begin Color := Message.wParam; end;
The last thing to do is send the message to the form. In Delphi, there are three possible ways of sending a message. Besides the VCL Perform method, you can also use the SendMessage and PostMessage Windows API functions.
The SendMessage function is used when the message needs to be handled as soon as possible. When you use the SendMessage function, it doesn't return until the message is processed.
The PostMessage function is used to send the message when its processing isn't time-critical. A message sent using the PostMessage function is placed in the window's message queue and is handled later.
The most straightforward way to send a message in a VCL Forms application is to call the Perform method. You should use the Perform method when you know exactly which control should process the message.
Here's how you can send the MM_CHANGECOLOR message to the main form and instruct it to change its color to white (see Figure 20-7):
Figure 20-7: Using the MM_CHANGECOLOR custom message
procedure TForm1.Button1Click(Sender: TObject); begin Perform(MM_CHANGECOLOR, clWhite, 0); end;
If you want to use the SendMessage and PostMessage API functions to send messages, you'll have to pass the handle of the destination window as the first parameter. The window handle of the main form is specified in its Handle property.
SendMessage(Handle, MM_CHANGECOLOR, clWhite, 0);
The messaging system is very flexible and allows us to do whatever we want with our custom messages. For instance, we can even use messages to add text from a text box to a list box. This can be done by typecasting the components to integers and then sending them as the wParam and lParam fields in the message.
Listing 20-20: Playing with messages
const MM_ADDTOLIST = WM_USER + 2; type TForm1 = class(TForm) public procedure MMAddToList(var Message: TMessage); message MM_ADDTOLIST; end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.MMAddToList(var Message: TMessage); var edit: TEdit; begin edit := TEdit(Message.LParam); TListBox(Message.WParam).Items.Add(edit.Text); end; procedure TForm1.Button1Click(Sender: TObject); begin SendMessage(Handle, MM_ADDTOLIST, Integer(ListBox1), Integer(Edit1)); end;