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 handle the WM_DROPFILES
message (rock particles).
IIA. Instance Subclassing -- the API
Approach Let us first perform the
technique of instance subclassing via the SetWindowLong API function.
By specifying the GWL_WNDPROC value to this function, we can specify a new window
procedure for Memo1. The function will return the
address of Memo1's default window procedure. The code for this example program
is provided in Listings 1.18a and 1.18b.
The first thing to notice in the implementation of our
example program is that we did not have to create a TMemo
descendant class. That is, we
have handled Memo1's
WM_DROPFILES
message in our implementation of the TForm1
class. The private members of
our TForm1
class include a variable of type FARPROC
named Memo1DefaultWndProc. This member will hold the address
of Memo1's
default window procedure. We
also have a generic pointer named MemberFunctionInstance. This will point to a non-member
function that will map to our Memo1WindowProc
member function. In addition,
we have a private member function, HandleDroppedFiles,
that will serve to perform the decoding of the WM_DROPFILES
message and load the file into our Memo
control.
In our form's constructor, we first use the DragAcceptFiles
API function, passing in the Handle
of our Memo
control, to register Memo1
as a valid drop target. Next,
we use the MakeObjectInstance
VCL function to create a non-member function that maps Memo1's
messages to our TForm1::Memo1WindowProc
member function. Again, this
is necessary as a window procedure cannot be a (non-static) member
function of a class. As
discussed previously, the TWinControl
class performs this same technique for all windowed VCL controls. The MakeObjectInstance
function returns a pointer to this non-member function, which is then
specified as the address of Memo1's
new window procedure in the call to the SetWindowLong
API function. This latter
function returns the address of Memo1's
default window procedure which we store in our Memo1DefaultWndProc
member pointer. We need to
store the address of the default window procedure so that we can remove the subclass of our Memo
control before it is destroyed.
We also store the address of the default window procedure so that
we can call it directly via the CallWindowProc
API function so as to pass all other messages on to the default window
procedure.
Our TForm1::Memo1WindowProcedure
member function is termed the subclass procedure. All messages directed at Memo1
will pass through this member function. In this way, we can handle Memo1's
WM_DROPFILES
message in our implementation of the TForm1
class – no TMemo
descendant was necessary. The
last function call in our subclass procedure is to the CallWindowProc
API function, which serves the purpose of passing on messages to the Memo
control's default window procedure.
For example, it we were to trap the WM_PAINT
message, the Memo
control would never paint its client area, eventually resulting in a stack
overflow (i.e., our application's message queue would be flooded with
un-handled WM_PAINT
messages for the Memo). This latter situation is analogous
to John cutting his fuel line and inserting the coffee filter, but never
reconnecting the two ends.
The switch
block within our subclass procedure tests only two cases of the Msg
parameter, WM_DROPFILES
and WM_DESTROY. It is in response to the latter
that we remove the instance subclass. For the WM_DROPFILES
case, we call our HandleDroppedFile
member function which performs the actual work of decoding the TMessage
parameters and loading the file into the Memo control. Notice that this message is
trapped (i.e., it is not passed on to the default window procedure) since
the TMemo
class and the underlying Windows EDIT
standard control (of which TMemo
is based) has no use for the WM_DROPFILES
message. (In general, it's best not to trap a messages unless you
know that it's not needed by the underlying classes/control.)
Within our HandleDroppedFile
member function, we retrieve a handle to the drop object from the WParam
member of the Msg
parameter. Like the mouse
cursor coordinates extracted in our WM_LBUTTONUP
message handler, this is another example of how Windows encodes vital
information within the wParam
and lParam
data members. We then call
the DragQueryFile
API function to retrieve the number of files dropped and the name and path
of the file. The text is then
loaded into our Memo
control via the TStrings::LoadFromFile
member function (via the TMemo::Lines property). Finally, the drag object handle is
closed via the DragFinish
API function.
Keep in mind that the details of the implementation of
our HandleDroppedFile
member function are not of importance here. The idea is that we have an
alternative method of handling messages sent to a particular window. The technique of instance
subclassing is especially useful for those controls where only a single
message need be handled, and vital when one needs to handle messages for a
control created without the VCL.
IIB. Instance Subclassing -- the VCL Approach
As with many aspects of the Windows API, the VCL offers
an easier alternative to instance subclassing. The TControl
class introduces the WindowProc
property that can be used directly for the task of instance
subclassing. We first saw
mention of the WindowProc
property in our discussion of the TWinControl::MainWndProc
member function. This
property is simply a pointer to the TControl::WndProc
member function of type TWndMethod. The difference between WindowProc
property and the WndProc
member function is that the latter is protected and thus not accessible
outside the scope if its class or descendants. On the other hand, the WindowProc
property is declared public and can thus be used for the purpose of
instance subclassing.
Listings 1.19a and 1.19b provide a modified version of our previous
example using the TControl::WindowProc
property.
The WindowProc
property allows us to perform an instance subclass in an easier fashion
than the API approach involving the SetWindowLong
function. Further, as the
WindowProc
property is of type TWndMethod,
we do not have to use the MakeObjectInstance
function. Another advantage
in the WindowProc-based approach is
that we did not have to use the CallWindowProc
API function to pass on the message to the default handler. In this case, we used the Memo1DefaultWindowProc
member as a direct function call.
Aside from the aforementioned advantages, the two techniques are
functionally similar (i.e, either approach will yield the same end
result).
III. Choosing the Correct Message-Handling Approach
We have discussed three methods of handling messages
sent to a window. Most often,
the virtual Dispatch
member function is augmented via the BEGIN_MESSAGE_MAP,
MESSAGE_HANDLER,
and END_MESSAGE_MAP
macros. Augmenting the WndProc
member function is usually done when a message needs to be handled before,
or trapped from, the WndProc
function of the ancestor class.
Finally, instance subclassing is most commonly used when only a
single message need be handled, or in situations where the previous
approaches are not practical.
It is important to note that there is one major
advantage presented by augmenting the WndProc
member function or performing an instance subclass, over that of using the
BEGIN_MESSAGE_MAP,
MESSAGE_HANDLER,
and END_MESSAGE_MAP
macros. Namely, the advantage
lies in the area of message trapping. Recall that the last function call
in the TControl::WndProc
function definition is to the virtual Dispatch
member function. Also recall
that in using the message mapping macros, we are augmenting this Dispatch
function. By doing this, we
are processing messages after
they have been handled by the TControl::WndProc
member function. On the other
hand, by performing an instance subclass or by augmenting the WndProc
function, we have access to the messages before or after the WndProc
function handles it. To
demonstrate this idea, let us examine one last example.
Consider a single form containing a TPanel, Panel1, and a TShape, Shape1. The Shape control is parented to the Panel and has its Align property set to alClient. The Panel is a TWinControl descendant while the Shape is a TGraphicControl descendant. Recall that the TWinControl::WndProc function definition of Listing 1.8 calls the IsControlMouseMsg function to determine whether to process mouse message or send them to a child control. In this case, since the Shape occupies the entire client area of the Panel, all of the mouse messages (resulting from mouse input directed to the Panel's client area) that are a normally sent to the Panel will be directed to the Shape (via the TControl::Perform member function). That is, none of the Panel's events that originate from mouse message will fire their associated handlers. This situation is depicted in Figure 1.5.
Normally, in this situation, one would simply use the mouse-related events of the Shape instead of those of the Panel. However, some TGraphicControl descendants do not publish mouse-releated events. As such, let us consider some other alternatives so that we can use the Panel’s mouse events.
The simplest solution, in this case, is to simply
disable the Shape
(i.e., set its Enabled
property to false). We know this will work since the
IsControlMouseMsg
function will return false
for disabled controls. That
is, the IsControlMouseMsg
function uses the TWinControl::ControlAtPos
member function with a false
AllowDisabled
argument. However, this
approach is not entirely robust since some TGraphicControl
descendants render themselves differently when disabled.
Another alternative that may have come to mind is to
simply create a TPanel
descendant and handle the messages directly in the descendant's
implementation. The question
now becomes – "Which message handling scheme to use – augment the WndProc
member function or use the message mapping macros?" As it turns out, the answer is to
augment the WndProc
member function. Why? Well,
remember, the message mapping macros simply augment the Dispatch
member function, and the Dispatch
member function is called at the end of the TWinControl::WndProc
definition. However, we know
that for our case, the TWinControl::WndProc
member function will direct all mouse messages to the Shape
and thus never fire the Panel's
Dispatch
function. This means that
augmenting the Panel's Dispatch
member function will, in fact, have no effect because the mouse-related
messages have been "diverted" to the Shape. The code provided in Listings
1.20a and 1.20b illustrate this idea.
In Listing 1.20a, we use the message mapping macros to augment the Dispatch member function and handle the WM_MOUSEMOVE message for our TPanel descendant class. Unfortuately, our message handler, WMMouseMove, defined in Listing 1.20b will never be called. That is, the TPanel's, and thus our descendant's, Dispatch member function will never be called. In this case, the Caption of our Form will always begin with to "Shape Mouse Move: ".
Instead of using the message mapping macros, let us now
augment our Panel’s
WndProc
member function as illustrated in Listings 1.21a and 1.21b.
By augmenting the WndProc
member function, we have the chance of handling the messages sent to our
TPanel
descendant before the ancestor class processes them. In this case, our Form’s
Caption
will always begin with "Panel Mouse Move: ". Further, notice that the WM_MOUSEMOVE
message is trapped so as to prevent the WndProc
member function of the ancestor class from receiving the WM_MOUSEMOVE
message. For our case, this
is indeed necessary, as the TWinControl::WndProc
member function would ultimately fire the Shape's
OnMouseMove
event handler, which would then change the Caption
of our Form
to reflect the Shape's
mouse movement.
Further, notice the placement of the call to the TPanel::WndProc
function – it is placed last.
If we had called the WndProc
member function of the TPanel
class before our message handling block, the Shape's
OnMouseMove
event handler would still fire, changing the Form's
Caption. Our message handling block would
then fire, changing the Caption
to reflect mouse movement on the Panel. Obviously, this is not what we
want, as the Caption
would be changed twice in this case, and may result in flicker.
Lastly, notice that we could have used an instance
subclassing approach instead of augmenting the WndProc
member function. We know that
such an approach can also be used to intercept messages before they are
received by the WndProc
member function of the ancestor class.
In many situations, the choice of handling a message
before or after the WndProc
function of the ancestor class can become confusing. Oftentimes, the choice will depend
on the actual message and the type of processing that needs to be
done. The question to ask,
when handling a message, is whether or not the message need be processed
by the ancestor's WndProc
function, and if so, is the resulting action vital at the stage when the
message is handled in a subclass procedure or augmented
implementation.
IV. Summary and Remarks
The information presented here (and in the first
article of this series) should provide a solid foundation for
understanding the nature of Windows, window messages, and message
handling. The intent was to
provide an initial, yet fundamental link between the VCL and the Windows
API in the areas of messages and events. In forthcoming articles, we will
examine the second fundamental link between the VCL and API, that of
window creation and management.
|