In the previous article (see “From Messages to Events Part I”) we discussed the trail of member functions that a message traverses, and we also examined how specific messages can be handled. Let us now make the association between these messages and their corresponding VCL events.
I. The Link Between Messages and VCL Events
We will approach this by
returning to our simple example depicted in Figure 1.1 whose code is
provided here again in Listing 1.13 for convenience.
As mentioned, the OnClick
member function is triggered by the WM_LBUTTONUP
message. Namely, if the TMessage
structure with a WM_LBUTTONUP
Msg
member reports that the mouse was released within the bounds of the target
control, then a "click" occured. Using our message mapping knowledge, let
us implement such functionality without the use the TButton::OnClick
event. That is, we will
directly handle the WM_LBUTTONUP
message sent to the button's window procedure. This implementation is provided in
Listings 1.14a and 1.14b.
Figure 1.3 depicts this modified example.
The code provided in Listing 1.14, differs from our
initial example in several aspects.
First, we remove our TButton
component, Button1. In its place, we implement a TButton
descendant class called TMyButton,
and then we create an instance of this class, MyButton1,
in our form's constructor.
Notice that we used the message mapping macros in our TMyButton
class listing. That is,
unlike in the example provided in Listing 1.12a, we do not map the WM_LBUTTONUP
message at the “form level” (i.e., within the implementation of our TForm1
class). The reason for this
is that we want to catch the WM_LBUTTONUP
message that's sent to MyButton1's
window procedure, not the WM_LBUTTONUP
message sent to Form1's
window procedure. That is, we
augment the virtual TMyButton::Dispatch
member function, not the virtual TForm1::Dispatch
member function.
Within the MESSAGE_HANDLER
macro, we map the WM_LBUTTONUP
message to our message handler, WMLButtonUp
(named by convention).
Within this handler, we extract the mouse cursor coordinates from
the LParamLo
and LParamHi
TMessage
data members, use the TControl::ClientRect
property to retrieve the rectangle defining MyButton1's
client area, then test whether the mouse cursor was released within this
client area via the PtInRect
API function.
Notice that the first call in our message handler is to
the TButton::Dispatch
member function. That is, we
are effectively passing on the message to the TButton
class (i.e., not ending the message trail). In doing so, we allow the TButton
class to process the message as usual. For example, if we assign an event
handler to the TMyButton::OnClick
event (inherited from TButton),
it would still fired as expected because the TButton
class and its ancestor classes are still receiving the WM_LBUTTONUP
message. However, if we
remove the call to the TButton::Dispatch
member function in our WMLButtonUp
definition, the WM_LBUTTONUP
message would not be sent to the TButton
class, and thus the OnClick
event handler would never be called.
That is, we would have effectively blocked or "trapped" the WM_LBUTTONUP
message. Listing 1.15
illustrates the modified message handler.
Here, we set the Result
data member of the TMessage
parameter to zero
indicating that we are effectively replying to the WM_LBUTTONUP
message with a value of zero
(i.e., our button’s window procedure is returning identically zero). Notice that the return type to the
typical C-style window procedure definition of Listing 1.6 is an LRESULT. Most window messages require a
zero
reply value. Yet, recall that
we did not explicitly set the Result
value in the WMLButtonUp
definition of Listing 1.14b.
In this way, we preserved the Result
value that was specified by the TButton
(or its ancestor) class(es).
In fact, the Result
data member is initialized to zero
in the default window procedure, so the explicit assignment in Listing
1.15 is redundant. However,
by convention, a zero
(return) value is assigned to Result,
indicating that the message was trapped.
The idea of trapping a message may not, at first, seem
overly useful. However, when
special functionality is required from an existing windowed control, it is
oftentimes necessary from having the ancestor class's window procedure
process the message. For
example, if we had trapped the WM_LBUTTONDOWN
message instead of the WM_LBUTTONUP
message in Listing 1.15, we would have prevented our button from appearing
pushed when clicked.
We can begin to see here how the WM_LBUTTONUP message is related to the TControl::OnClick event. Namely, the TControl class handles the WM_LBUTTONUP message in much the same way that we did in Listing 1.14. A condensed C++ translation of the TControl::WMLButtonUp member function (message handler) is provided in Listing 1.16.
In many respects, the TControl::WMLButtonUp
message handler is similar to our WMLButtonUp
message handler of Listing 1.14b.
For example, the PtInRect
API function is also used here to test the location of the mouse
cursor. The SmallPointToPoint
VCL function returns a POINT
structure from the Pos
member of the Message
parameter. Here, instead of
the generic TMessage
type, the TControl
implementation uses a parameter of type TWMLButtonUp. The two are virtually identical
except that latter has members that correspond to the decoded wParam
and lParam
data members. In this case,
the Pos
member contains the same information stored in the LParamLo
(LOWORD)
and LParamHi
(HIWORD)
TMessage
members. As we initially saw
in Listing 1.12b, these members correspond the X
and Y
coordinates of the mouse cursor, repsectively (Note: LParamLo
and LParamHi
are message specific).
The TControl::WMLButtonUp
definition of Listing 1.16 also confirms the aforementioned claim that the
OnClick
and OnMouseUp
events originate from the WM_LBUTTONUP
message. The event handler
for the former, if assigned, is fired only if the mouse cursor was
released within the bounds of the client area. The task of calling the event
handler is accomplished via the TControl::Click
member function, translated and provided in Listing 1.17.
The private FOnClick
member is a pointer to the user-specified OnClick
event handler. Specifically,
FOnClick
is of type TNotifyEvent.
typedef
void __fastcall (__closure
*TNotifyEvent)(System::TObject* Sender);
Examining the above type definition, we can see that
the TControl::Click
member function will directly call the OnClick
event handler only if one has been assigned. Also, since the this
keyword is passed as the Sender
parameter of the event handler, we are able to perform the common task of
distinguishing the originator or "sender" of the event. Keep in mind, however, that TNotifyEvent
is not the only type of function pointer that is used to call event
handlers. Specifically, the
type of pointer will depend on the type of event and the amount of
information that need be relayed.
For example, the OnMouseDown
and OnMouseUp
events are of type TMouseEvent.
typedef
void __fastcall
(__closure
*TMouseEvent)(System::TObject* Sender, TMouseButton
Button, Classes::TShiftState
Shift, int X, int Y);
Through our simple example, we have now established the
link between Windows messages and VCL events. Moreover, we have made a crucial
step toward understanding the communication mechanism between Windows and
the VCL. As we will see
throughout the rest this text, most TWinControl events originate from a
specific Windows message.
In future articles, we will examine how the VCL defines and uses
its own custom messages to provide extended functionality. For now, let us cover the final
topic of this article – instance subclassing.
It may have come to mind, after examining the
implementation of our TMyButton class, that in order to handle
a message sent to a specific control, we need to augment the Dispatch
member function of that control's class. For example, in order to catch the
WM_LBUTTONUP message for our button, we
created the TMyButton class. One would be correct in arguing
that, we did not need to create the TMyButton class, as we could have used
the TButton::OnMouseUp event instead. In fact, in many situations, the
existing VCL events are sufficient.
However, if you've worked with the VCL for a while, you're probably
all too familiar with its shortcomings; namely, not all messages have
corresponding events. Indeed,
as the Windows API exposes more and more messages, the need for direct
message-handling increases as well.
Yet, creating a descendant of a existing component can oftentimes
be overkill for even the most complex applications. This is especially true if the
component is to be used only in one instance of a particular project. In fact, Windows presents a
technique called subclassing, that allows a direct tap into a
window's window procedure.
The VCL extends this idea by allowing subclassing of all TControl
descendants (i.e., including non-windowed controls).
As we will learn in the forthcoming article, the window procedure of a window is specified when the class of a window is initially registered with Windows. All controls of that class will use this function as the default window procedure. This is the same window procedure that is invoked by the CallWindowProc API function that is called by TWinControl::DefaultHandler member function. By using a technique called instance subclassing, we can provide a replacement window procedure for a specific instance of a window. To fortify the this idea and the usefuleness of the technique, let us consider a simple example.
Suppose we have a form, Form1,
containing a single TMemo
control, Memo1. This example is depicted in Figure
1.4.
We would like the Memo
control to be able to load text files easily. To this end, we want to be able to
drag and drop a file directly from Explorer into our Memo. An easy way to accomplish this is
to handle the WM_DROPFILES
message which is sent to a window when the user drags, then drops a file
within its bounds. Yet,
examining the various TMemo
events, we find that there is no VCL event such as OnDropFile
that corresponds to the WM_DROPFILES
message. One possible
solution is to simply create a TMemo
descendant and handle the WM_DROPFILES
message via a message mapping technique. Another approach would be to
create a TMemo
descendant and augment the virtual TWinControl::WndProc
member function. However,
seeing that we need to handle only this single message, let us take the
instance subclassing approach.
That is, we will replace the window procedure of only our one
instance of the TMemo
class, Memo1. To drive-home the idea of what we
will be doing by performing an instance subclass, let us consider a simple
analogy:
After programming for 14 hours straight, our
friend John decides to drive to the supermarket to buy some fresh coffee
and a pack of coffee filters.
On the way home, John notices that he is running low on fuel and
decides to stop at a local gas station. Having recently
What John did to solve his problem is not unlike the idea of instance subclassing. Think of the fuel in his fuel tank as the messages sent from Windows, and his carburetor as our Memo control. Further, think of the WM_DROPFILES message as the rock particles. In the same way that John wanted to trap the rock particles, we want to handle and trap the WM_DROPFILES message. John could have modified his carburetor to accept the rock particles in the same way that we could create a TMemo descendant that handles the WM_DROPFILES message. However, like John, we do not want to deal with this hassle. Instead, we decide to tap into the Memo control's message stream (fuel line leading into the carburetor) and specifically |