Inside the VCL: From Messages to Events Part II
By Damon Chandler

 

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.

 

 

 

//----------------------------------------------------------------------

#include <vcl.h>

#pragma hdrstop

 

#include "Unit1.h"

//----------------------------------------------------------------------

#pragma resource "*.dfm"

TForm1 *Form1;

//----------------------------------------------------------------------

 

__fastcall TForm1::TForm1(TComponent* Owner)

    : TForm(Owner)

{

}

//----------------------------------------------------------------------

 

void __fastcall TForm1::Button1Click(TObject *Sender)

{

    Edit1->Text = "Hello World!";   

}

//----------------------------------------------------------------------

 

Listing 1.13

 

 

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.

 

   

 

//----------------------------------------------------------------------

#ifndef Unit1H

#define Unit1H

//----------------------------------------------------------------------

#include <Classes.hpp>

#include <Controls.hpp>

#include <StdCtrls.hpp>

#include <Forms.hpp>

//----------------------------------------------------------------------

 

class TMyButton : public TButton

{

private:

    void __fastcall WMLButtonUp(TMessage &Msg);

public:

    __fastcall TMyButton(TComponent *Owner) : TButton(Owner) {};

 

BEGIN_MESSAGE_MAP

    VCL_MESSAGE_HANDLER(WM_LBUTTONUP, TMessage, WMLButtonUp)

END_MESSAGE_MAP(TButton)

};

//----------------------------------------------------------------------

 

class TForm1 : public TForm

{

__published:  // IDE-managed Components

    TEdit *Edit1;

private:      // User declarations

    TMyButton *MyButton1;

public:       // User declarations

    __fastcall TForm1(TComponent* Owner);

};

//----------------------------------------------------------------------

extern PACKAGE TForm1 *Form1;

//----------------------------------------------------------------------

#endif

 

Listing 1.14a

 

 

//----------------------------------------------------------------------

#include <vcl.h>

#pragma hdrstop

 

#include "Unit1.h"

//----------------------------------------------------------------------

#pragma resource "*.dfm"

TForm1 *Form1;

//----------------------------------------------------------------------

 

__fastcall TForm1::TForm1(TComponent* Owner)

    : TForm(Owner)

{

    MyButton1 = new TMyButton(this);

    MyButton1->Parent = this;

    MyButton1->Caption = "MyButton1";

    MyButton1->Left = Edit1->Left + Edit1->Width + 20;

    MyButton1->Top = Edit1->Top;

}

//----------------------------------------------------------------------

 

void __fastcall TMyButton::WMLButtonUp(TMessage &Msg)

{

    TButton::Dispatch(&Msg);

 

    POINT MousePos = Point(Msg.LParamLo, Msg.LParamHi);

    RECT R = static_cast<RECT>(ClientRect);

 

    if (PtInRect(&R, MousePos))

    {

        Form1->Edit1->Text = "Hello World!";

    }

}

//----------------------------------------------------------------------

 

Listing 1.14a

 

 

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.

 

 

 

void __fastcall TMyButton::WMLButtonUp(TMessage &Msg)

{

    // TButton::Dispatch(&Msg);

 

    POINT MousePos = Point(Msg.LParamLo, Msg.LParamHi);

    RECT R = static_cast<RECT>(ClientRect);

 

    if (PtInRect(&R, MousePos))

    {

        Form1->Edit1->Text = "Hello World!";

    }

    Msg.Result = 0;

}

 

Listing 1.15

 

 

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.

 

 

 

void __fastcall TControl::WMLButtonUp(TWMLButtonUp &Message)

{

    TComponent::Dispatch(&Message);

 

    //

    // mouse-capture-related stuff...

    // 

     

    if (ControlState.Contains(csClicked))

    {

        ControlState = ControlState >> csClicked;

        RECT R = static_cast<RECT>(ClientRect);

 

        if (PtInRect(&R, SmallPointToPoint(Message.Pos)))

        {

            // will fire the OnClick event

            // handler, if assigned

            Click();

        }

    }

    

    // will fire the OnMouseUp event handler, if assigned

    DoMouseUp(Message, mbLeft);

}

 

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.

 

 

 

void __fastcall TControl::Click{)

{

    if (FOnClick) FOnClick(this);

}

 

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.

 
II. 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 wasted spent all of his funds of the newest version of Delphi, he is forced to buy the ultra-cheap gas.  Twenty miles after leaving the gas station, John's car starts to hesitate and he concludes that the 65 octane fuel (hey, it was ultra-cheap) contains rock particles.  John pulls over to make repairs but realizes that he does not have the technical skills necessary to modify his engine to accept rocks.  He does however, notice that he can grab hold of a section of the rubber fuel line that leads into the carburetor.  John decides to cut the fuel line and insert a coffee filter in the hopes of trapping the rock particles.  After performing the delicate surgery, John starts his car and successfully drives home.

 

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