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
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
control's message stream (fuel line leading into the carburetor) and
specifically handle the WM_DROPFILES
message (rock particles).
IIA. Instance Subclassing -- the API
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
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.
Copyright © 2004 Damon Chandler; All rights reserved.