Inside
the VCL: From Messages to Events Part
I |
Most introductory C++ courses fortify the ideas of syntax, object-oriented design, coding styles, and a plethora of other topics that serve to form a solid foundation for the language itself. Indeed, such a fortification is neccessary to mastering any progamming language. However, being an apt C++ programmer is far from being a strong Windows developer. It isn’t until one embarks upon the journey of implementing an event-driven program that a true appreciation of the idea of a window-based environment can be gained. Indeed, Windows itself is based upon a message or “signal” oriented framework that lends to a better understanding of the system as a whole. These signals form the foundation of Windows’ multi-tasking capabilities.
The fact that C++Builder encapsulates most the API leads many to believe that there is some type magic going on behind the scene in the VCL. In fact, although it is sometimes a subtle point, a typical VCL application contains all the necessary underlying API work. The level of abstraction exhibited by the VCL presents us with an intuitive, easily maintanable framework, of which the Rapid Application Development (RAD) theme is based. Nonetheless, a true understanding of the VCL cannot be appreciated without some knowledge of the API itself. The purpose of this article is to introduce the basic constructs of a VCL application from an API point of view. Mastery of the Windows API is surely not a prerequisite here. As such, many of the presentations of this first article will be preceeded by a high-level introduction of the underlying API methods. The intent here is introduce the inner workings of the TApplication and TWinControl classes from an API point of view; a look “under the hood” if you will. This, in turn, will allow a common foundation of terminology and notation from which subsequent presentations can be based.
I. An Event-Driven Implementation
Recall,
the basic “Hello World!” program which simply outputs this, almost ubiquitous,
single line of text. For completeness,
this is provided in Listing 1.0.
int main(int argc, char
*argv[]) { std::cout
<< "Hello World!" <<
std::endl; return 0; } |
Listing 1.0
|
Common
knowledge tells us that this is clearly not an event-driven program. That is, a certain event does not trigger a certain
action at any given time. Imagine now,
a primitive attempt to extend this simple program toward an event-driven
implementation as in Listing 1.1.
int main(int argc, char*
argv[]) { while (getch() != 'Q') { std::cout
<< "Hello World!" <<
std::endl; } return 0; } |
Listing 1.1
|
Here,
we see that the program will simply output the phrase, “Hello World!”, for
every keystroke, until the user enters the letter “Q”. While this is far from a de facto event-driven
program, it lends to the idea of an “action / reaction” implementation. That is, for every user action, there is
some reaction. Sometimes this reaction
is simply the feedback of textual information as in our simple program
above. Sometimes the reaction will be
the output of a printed page of text.
Sometimes the reaction will be as complicated as a change in the
direction of an animated figure in a graphically intense 3-D game. Perhaps even the output will be nothing at
all. The idea here is that the user
implicitly defines the reactions or “path” of the program through his or her
own actions.
As mentioned, Windows is an event-driven, message based environment. Clicking the Start Button, for example, will cause the Start Menu to appear. Like before, this is an example of an “action / reaction” implementation, which happens to be the standard behavior of most Windows programs. As we will shortly see, the actual main processing in any Windows program resembles the form of our simple “Hello World!” extension.
To begin to understand the fundamental connection between a text-based C++ application and an event-driven Windows application, let us first examine a typical application message loop, commonly referred to as the main message “pump” as provided in Listing 1.2.
WINAPI APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE
hPrevInstance, LPTSTR
lpCmdLine, int nCmdShow) { // // <window
creation code> ... // MSG Msg; while (GetMessage(&Msg, NULL, 0,
0)) { TranslateMessage(&Msg);
DispatchMessage(&Msg); } return 0; } |
Listing 1.2
|
The
first thing to notice from Listing 1.2 is that the familiar main function of Listing 1.1 is
replaced by its Windows equivalent, WinMain. From
within the implementation of this WinMain function, we can begin to
see how a Windows application can be based upon events. Just as the getch function of Listing 1.1
served to retrieve a character from the keyboard, the GetMessage API function is designed to
retrieve a message. These messages
originate from the system – the keyboard driver, mouse driver, and other device
drivers.
In
fact, the implementation of our WinMain function consists of nothing more than what
may at first seem like an infinite while loop.
This latter statement would certainly be accurate if the GetMessage function were to always
return true. In fact, the GetMessage function returns true for all messages except WM_QUIT. The purpose of this function is to simply retrieve a message from
the application’s message queue (filled by Windows). Again, this is working in much the same way as the getch function from our “Hello
World!” example.
The
TranslateMessage API function serves to
interpret keyboard input messages. Specifically,
this function converts virtual-key code messages (WM_KEYDOWN, WM_KEYUP) into the more useful (WM_CHAR) character value
message. It the then the job of the DispatchMessage API function to dispatch or
"send" the message to the correct window. Specifically, this information is sent to a special callback
function known as a window procedure.
The DispatchMessage function serves a role analogous to that of the cout statement of Listing1.1,
except that DispatchMessage directs a message to a window procedure instead of
directing text to an iostream. The message
retrieval/transmission cycle
continuously repeats until the WM_QUIT message is received (i.e., the GetMessage function returns false, breaking the while loop, or “terminating the
pump”).
The
information that the GetMessage function retrieves is placed in a MSG structure. The six data members of this structure serve
to inform an application of a particular event.
typedef struct
tagMSG {
HWND
hwnd;
UINT
message;
WPARAM wParam;
LPARAM lParam;
DWORD
time;
POINT
pt;
}
MSG;
The
hwnd data member contains the handle of the window that
the message is intended for. Again,
this is used by the DispatchMessage API function to determine to which window's window
procedure to send the mesasge. The message data member contains the actual message constant
(e.g., WM_QUIT). The wParam and lParam members are 32-bit message parameters that may
contain useful information about the specifics of the message. For example, the wParam member of the WM_QUIT message contains an integer
value identifying the “exit code”. The time data member identifies the value of the system
clock when the message was posted, while the pt member contains the
location of the mouse cursor at this time.
II. Communication via Messages
As
mentioned earlier, the window procedure of a window receives any messages that
are sent to it. Most of these messages
will be sent by Windows itself, either directly to the window procedure, or
indirectly through our main message pump.
Messages that pass through the message pump are called queued
messages, and are commonly referred to as “posted” messages. These typically include messages resulting
from user input via the mouse or keyboard.
Painting messages, such as WM_PAINT, are also queued, as are system timer (WM_TIMER) and termination (WM_QUIT) messages. Most other messages are sent directly to a
window’s window procedure. These latter
messages are called non-queued messages.
To get a better understanding of window messages, let us consider a simple VCL application consisting of a single form with a TButton control and a TEdit control parented to it, as depicted in Figure 1.1. The code for this example is provided in Listing 1.3.
//---------------------------------------------------------------------- #include <vcl.h> #pragma hdrstop #include "Unit1.h" //---------------------------------------------------------------------- #pragma package(smart_init) #pragma resource "*.dfm" TForm1 *Form1; //---------------------------------------------------------------------- __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } //---------------------------------------------------------------------- void __fastcall TForm1::Button1Click(TObject*
Sender) { Edit1->Text
= "Hello World!"; } //---------------------------------------------------------------------- |
Listing 1.3
|
In fact, our entire implementation consists of nothing more than a single event handler for the TButton::OnClick event, in which we simply maniuplate the TEdit::Text property. We see here that when the button is clicked, the edit control’s text is changed to the value “Hello World!”. This is another example of an “action / reaction” implementation. The action being the user clicking the button control, while the reaction is a change in the edit control’s text.
While this simple program is far from being a sophisticated application, we get first hand evidence here of the level of abstraction presented by the VCL. Namely, unlike in a raw API program, we did not have to send any window messages to the edit control itself, nor did we have to tap into the button control’s window procedure and respond to the WM_LBUTTONUP message. Further, we did not have to code the standard windows message pump. How then can this program operate in an event driven fashion? To see how this is accomplished, we need to examine our project’s source code as provided in Listing 1.4.
//---------------------------------------------------------------------- #include <vcl.h> #pragma hdrstop //---------------------------------------------------------------------- USEFORM("Unit1.cpp",
Form1); USERES("Project1.res"); //---------------------------------------------------------------------- WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { try {
Application->Initialize();
Application->CreateForm(__classid(TForm1),
&Form1);
Application->Run(); } catch (Exception &exception) {
Application->ShowException(&exception); } return 0; } //---------------------------------------------------------------------- |
Listing 1.4
|
Notice
that our project’s source code contains the aforementioned WinMain function. Specifically, this is the exact same WinMain function as in Listing
1.2. The main difference here is that
the entire message loop, window creation, and exception handling mechanisms are
encapsulated by the TApplication and TWinControl classes. In fact, the standard
Windows message loop is entered via the TApplication::Run member function. We can begin to see here that a VCL
application is indeed a standard Windows application consisting of a WinMain function and a message
loop.
Now
that we have convinced ourselves that the standard message pump is indeed
present, let us return to our TForm1 class implementation and examine how the TWinControl class relieves the hassle
of dealing with messages, including those dispatched from the Application object.
In
Listing 1.3 our button’s OnClick event handler consists of a single line of
code, which simply changes the edit control’s text via the TWinControl::Text property. Yet, in an API implementation, to actually
change the text of any windowed control, the WM_SETTEXT message is used, as demonstrated
in Listing 1.5.
void __fastcall TForm1::Button1Click(TObject*
Sender) { // Edit1->Text
= "Hello World!";
SendMessage(Edit1->Handle, WM_SETTEXT, 0, reinterpret_cast<LPARAM>("Hello World!")); } |
Listing 1.5
|
Here
we use the SendMessage API function to directly send the edit control’s window procedure the WM_SETTEXT message. Not only is this code hard to maintain, it
is less concise and not as type-safe as the TWinControl::Text Property. The latter simply encapsulates the SendMessage call via the TWinControl::SetText member function. In fact, the TWinControl class allows manipulation
of many window attributes through the use of its member functions. There are very few occassions where we will
need to explicitly use the SendMessage API function.
We
now know that changing a TWinControl’s Text property will indirectly
send the
WM_SETTEXT
message to the control itself. However,
we have yet to examine the messages associated with our button’s OnClick event. Specifically, let us explore the messages
that are sent to the button when it is clicked, and examine how these messages
relate to our Button1Click event handler.
As previously stated, messages that are directed to a window are sent to a special function called a window procedure. For example, when we used the SendMessage API function, in Listing 1.5, we sent our edit control’s window procedure the WM_SETTEXT message. In turn, the edit control handled this message by changing its text. In much the same way, when our button is clicked we indirectly handled the WM_LBUTTONUP message through our button’s OnClick event handler. The difference here is that Windows itself, indirectly sent our Button the WM_LBUTTONUP message. The TWinControl class handled this message by testing the location of the mouse cursor, then appropriately calling our Button1Click member function.
In general, when the left mouse button is depressed while the mouse cursor is within the bounds of any enabled windowed control within our application, Windows sends the WM_LBUTTONDOWN message to our message pump. The DispatchMessage function within our message loop then directs this message to the control’s window procedure. Similarly, if the left mouse button is then released, the control is indirectly sent the WM_LBUTTONUP message. These two messages are examples of queued messages, and are commonly used to define a mouse “click”. Typically, these messages are explicitly handled in the control’s window procedure as demonstrated in Listing 1.6.
LRESULT CALLBACK ButtonWndProc( HWND hwnd, // handle of window UINT uMsg, // message identifier WPARAM
wParam, // "word" message parameter LPARAM
lParam) // "long" message parameter { switch (uMsg) { case WM_LBUTTONDOWN: {
SetCapture(hwnd); break; } case WM_LBUTTONUP: {
ReleaseCapture(); SendMessage(hwndEdit,
WM_SETTEXT, 0,
reinterpret_cast<LPARAM>("Hello World!")); break; } default: return DefWindowProc(hwnd, uMsg, wParam,
lParam); } return 0; } |
Listing 1.6
|
The
OnClick event, however, saves us
the hassle of implementing a window procedure as that of Listing 1.6. This is especially useful when a control need
only respond to a single user event.
We will examine the specifics of the TButton and TEdit classes in subsequent
articles.
III. Window Procedures and VCL Events
In
the previous section, we discussed queued and non-queued messages, and how
specific messages can be used to invoke certain changes. For example, we used to the WM_SETTEXT message to change the text
entry in our edit control. Similarly,
we could have used the WM_SETFONT message to change the font
of this text. We also implemented an OnClick event handler for a TButton control, and examined how
this event related to the underlying window messages WM_LBUTTONDOWN and WM_LBUTTONUP. Let us now investigate, in-depth, how these events are triggered
and how we can manipulate the fashion in which the messages that trigger these
events are handled. In this section and
the next (Message Handling), we will discover how user-defined event handlers
are triggered in response to window messages.
Recall,
our simple program of Listing 1.3 consisting of a single form with a TEdit control and a TButton control parented to
it. Focussing on the TButton control, let us inspect the
various events published in the Object Inspector, as depicted in Figure
1.2.
Our
goal is to understand the origin of these events; namely which window messages
are responsible for which events. The
drag-drop events rely on internal VCL messages and will be discussed in later
articles. As we discuss each of the
other events and their related messages, do not attempt to memorize the
specifics. In the forthcoming series of
articles, we will closely examine the details of each of these events and how
the TWinControl class handles the related messages. The idea here, is to gain an understanding of the relationship between
Windows messages and VCL events.
IIIA. Common VCL Events and their Related Messages
Since
we already know that the WM_LBUTTONUP message fires the OnClick event, let us examine the OnEnter and OnExit events. When a windowed control receives keyboard or
“input” focus, its window procedure is sent the WM_SETFOCUS message. For example, notice that our TButton oftentimes displays a
selection rectangle that surrounds its client area, as depicted in Figure
1.1. This rectangle serves to indicate
that the button has keyboard focus, and is rendered in response to the WM_SETFOCUS message. When the button loses keyboard focus (i.e.,
another windowed control receives keyboard focus), the button is redrawn
without the selection rectangle. This
latter task is performed in response to the WM_KILLFOCUS message. For TWinControl descendants, both the OnExit and OnEnter events are triggered by the
WM_SETFOCUS message. While this may seem counterintuitive, we
will see in later articles how this functionality is accomplished by TCustomForm::SetFocusedControl member function. For now, it is sufficient to understand that
we can provide a handler for the button’s OnEnter event to perform special processing when the
button receives keyboard focus.
Similarly, we can use the OnExit event to perform processing when the button
loses keyboard focus.
The OnKeyDown, OnKeyPress, and OnKeyUp events work in much the same way as the OnEnter and OnExit events. Namely, they are fired in response to a certain window message. In this case, the messages are WM_KEYDOWN, WM_CHAR, and WM_KEYUP, respectively. The WM_KEYDOWN message is sent to the window procedure of a window that has input focus when a non-system key is pressed. For example, pressing the letter ‘A’ when our button has keyboard focus will cause Windows to send our message pump the WM_KEYDOWN message (remember, this is a keyboard message, and is thus a queued message). The DispatchMessage function within our message loop will then send the WM_KEYDOWN message to our button’s window procedure. The OnKeyDown event is fired in response to this message. In the same fashion, the WM_CHAR and WM_KEYUP messages are responsible for the OnKeyPress and OnKeyUp events, respectively.
While
the OnKeyDown, OnKeyPress, and OnKeyUp events correspond to
keyboard input, the OnMouseDown, OnMouseMove, and OnMouseUp events are fired in response to mouse input. OnMouseDown is fired in response to the WM_LBUTTONDOWN, WM_MBUTTONDOWN, or WM_RBUTTONDOWN messages. OnMouseMove is fired in response to the
WM_MOUSEMOVE message. OnMouseUp is fired in response to the WM_LBUTTONUP, WM_MBUTTONUP, or WM_RBUTTONUP messages. Similarly, the OnDblClick event is fired in response
to the WM_LBUTTONDBLCLK message.
While
we have discussed which of our button’s events correspond to which window
messages, we still have yet to investigate how exactly these events are
triggered. Since the messages are sent
to the window procedure of our button, we first need to examine the TWinControl::WndProc member function. This latter function is simply a member
function that is called from the TWinControl::MainWndProc member function. As mentioned, all messages that are sent to
a window are ultimately sent to its window procedure, which is simply a
non-member callback function that defines how the window responds to certain
messages. We saw in Listing 1.6 an
example of a window procedure that defines how a control responds to the WM_LBUTTONDOWN and WM_LBUTTONUP messages.
IIIB. Inside the Window Procedure
As
mentioned, a window procedure is a non-member callback function that receives
all messages sent to a windowed control.
That is, it cannot be a (non-static) member function of a class. On the contrary, the MainWndProc and WndProc functions are member
functions of the TWinControl class. The VCL performs this
slight-of-hand via the MakeObjectInstance function, defined in the
forms.pas unit. This latter function
uses the VirtualAlloc API function to map a non-member window procedure to the TWinControl::MainWndProc member function. In turn, this member function calls the
WndProc member function, protected around an Object Pascal try/except block, passing all
exceptions to the TApplication::HandleException member function.
What
we see here amongst a maze of functions is the VCL’s attempt to encapsualte the
C-style Windows messaging system in an object-oriented framework. In the next series of articles, we will
explore how the TWinControl class encapsulates the process of window
creation. For now, let us focus on the MainWndProc and WndProc member functions. A condensed C++ translation of the Object
Pascal TWinControl::MainWndProc member function is provided
in Listing 1.7
void __fastcall
TWinControl::MainWndProc(TMessage& Message) { try { try {
WndProc(Message); } catch (...) {
// // some clean-up routines...
// }
// // some clean-up routines...
// } catch (...) {
Application->HandleException(this); } } |
Listing 1.7
|
We
see here that the MainWndProc member function uses the VCL TMessage structure.
This is similar to the API MSG structure, except the hwnd, time, and pt data members are excluded.
#pragma pack(push, 1)
struct TMessage
{
unsigned
long Msg;
union
{
struct
{
WORD WParamLo;
WORD WParamHi;
WORD LParamLo;
WORD LParamHi;
WORD ResultLo;
WORD ResultHi;
};
struct
{
long WParam;
long LParam;
long Result;
};
};
};
#pragma pack(pop)
Take note of the #pragma pack directives that surround the TMessage structure definition. These simply serve to instruct the compiler to use single-byte data alignment such that the various TMessage data members will directly correspond to their associated members of the MSG structure.
Sidebar – A
window procedure for non-windowed controls?
|
|
The
TWinControl::WndProc
member function
passes most messages on to the default handler for the anscestor class. For example, the TButton control requires very
little intervention from the VCL, as most of the default functionality is built
into the underlying BUTTON standard control. A pseudo-C++
overview of the TWinControl::WndProc member function is provided in Listing 1.8.
void __fastcall TWinControl::WndProc(TMessage&
Message) { switch (Message.Msg) {
// ... case //
most mouse messages...
{
TWMMouse WMMouse;
WMMouse.Pos = PointToSmallPoint( Point(Message.WParamLo,
Message.WParamHi));
WMMouse.XPos = Message.WParamLo;
WMMouse.YPos = Message.WParamHi; if (IsControlMouseMsg(WMMouse)) { if (WMMouse.Result == 0) DefWndProc(Handle, Message.Msg, Message.WParam, Message.LParam); return; } break; } case //
most keyboard messages... { if (Dragging) return; break; }
// ... }
TControl::WndProc(Message); } |
Listing 1.8
|
While
the implementation of the TWinControl::WndProc member function may seem
arbitrary at first, the thing to note is that the VCL handles only the messages
necessary to maintain functionality with other VCL classes; particularly, the mouse messages, WM_MOUSEMOVE through
WM_MBUTTONDBLCLK. It is the handler for these messages that
allows mouse messages to be relayed to non-windowed controls.
Sidebar – How
non-windowed controls respond to mouse messages.
|
Notice
that the TWinControl::IsControlMouseMsg function is called in
response to all mouse messages. This
function simply performs internal hit-testing so that only the mouse events
directed to the windowed control itself are processed by the window’s default
window procedure. Again, this is
necessary only to accomodate for the ability of TGraphicControl descendants to publish
mouse-input-related events. For
example, consider a TPanel with a TImage parented to it. Here, the TPanel is a TWinControl descendant (i.e., a true
windowed control with a window handle), while the TImage is a TGraphicControl descendant (no window
handle). Since Windows has no idea of
the notion of graphical controls, the TImage control cannot receive
any window messages. (Recall, the DispatchMessage function within our main
message loop uses the handle of the window specified in the MSG structure to determine
which window procedure to direct the message to). When the mouse cursor is moved over the Image, and thus over the Panel, the Panel’s WndProc member function, not the Image’s, will receive the WM_MOUSEMOVE message. Yet, the TImage class publishes the OnMouseMove event. Here is where the hit-testing and thus the
IsControlMouseMsg function comes into play. As in Listing 1.8, when the Panel’s WndProc member function receives
the WM_MOUSEMOVE message, it calls the IsControlMouseMsg function. It is the job of this latter function to
determine if a child controls exists at the location of the cursor, and if
so, pass the WM_MOUSEMOVE message to the child
control, in this case the Image. In
turn, the TControl::WndProc member function will
receive the WM_MOUSEMOVE message and ultimately
fire the Image’s OnMouseMove event handler, if one is
assigned. The same sequence applies
to other mouse messages as well. It
is in this way that TGraphicControl descendants can respond
to mouse events. |
Notice
the last function call in the TWinControl::WndProc definition of Listing
1.8. Here, the TControl::WndProc member function is called,
allowing all unhandled messages to be passed on to this latter function. Listing 1.9 contains an overview of the TControl::WndProc member function.
void __fastcall TControl::WndProc(TMessage&
Message) { // //
(1) handle design-time mouse messages... // // //
(2) handle keyboard messages... // // //
(3) handle run-time mouse messages...
//
// (a) handle double
clicks...
// (b) send hint messages for
Application hints
// (c) support dragging and
ControlStyle //
Dispatch(&Message); } |
Listing 1.9
|
Among
other things, it is the job of the TControl::WndProc member function query the
messages to maintain its ControlState property, provide
information back to the TApplication object for hint window
functionality, and filter out messages according to the TControl::ControlStyle property. The function then calls the Dispatch member function, which
passes the message on to either a specific message handler (if one has been
specified) or the DefaultHandler member function.
For TWinControl descendants, the TWinControl::DefaultHandler member function uses the CallWindowProc API function to call the
default window procedure of the underlying window class. This ends the chain of functions that a
window message can traverse.
We
have successfully traced the path of a typical window message through the
various VCL member functions. For
queued messages, this path starts with the TApplication::HandleMessage member function, whereas
for non-queued messagse, this path starts with the TWinControl::MainWndProc member function. Both types of messages will ultimately end
with the DefaultHandler member function.
In fact, not all messages need be passed on to the default window
procedure. Oftentimes a certain message
will be trapped (i.e., handled internally), effectively bypassing the default
window procedure. In the proceeding
sections, we will examine three techniques used to explicitly handle messages
and later expand upon the idea of message trapping.
IV. Message Handling
In the
last section we discussed how a window procedure handles certain messages and
specifically how the TWinControl class encapsulates this window procedure in the MainWndProc and WndProc member functions. When a certain message need be handled, the
logical approach is to simply tap into the WndProc member function. As it turns out, there are several different
approaches to handling specific messages.
Let us now examine each of these techniques – this will give us an insight
into how messages are handled. Later we
will return to our VCL “Hello World!” example, and use our message handling
knowledge to gain a true understanding of how VCL events are fired in response
to certain messages.
IVA. Augmenting the WndProc Member Function
Recall
that the TControl::WndProc member function is a virtual member function. It then follows that if we need to handle
any specific (or all) messages sent to a particular control, all we need to do
is augment (or override) that control's WndProc member function. Listings 1.10a and 1.10b provide an example that illustrates this
idea.
//---------------------------------------------------------------------- #ifndef Unit1H #define Unit1H //---------------------------------------------------------------------- #include <Classes.hpp> #include <Controls.hpp> #include <Forms.hpp> //---------------------------------------------------------------------- class TForm1 : public TForm { private: // User
declarations void __fastcall DoMouseUp(TMessage& Msg); protected: virtual void __fastcall
WndProc(TMessage& Msg); public: // User
declarations __fastcall TForm1(TComponent* Owner); }; //---------------------------------------------------------------------- extern PACKAGE TForm1 *Form1; //---------------------------------------------------------------------- #endif |
Listing 1.10a
|
//---------------------------------------------------------------------- #include <vcl.h> #pragma hdrstop #include "Unit1.h" //---------------------------------------------------------------------- #pragma resource "*.dfm" TForm1 *Form1; //---------------------------------------------------------------------- __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } //---------------------------------------------------------------------- void __fastcall TForm1::WndProc(TMessage& Msg) { switch (Msg.Msg) { case WM_LBUTTONUP: {
DoMouseUp(Msg); break; } } TForm::WndProc(Msg); } //---------------------------------------------------------------------- void __fastcall TForm1::DoMouseUp(TMessage& Msg) { int X = Msg.LParamLo; int Y = Msg.LParamHi; ShowMessage("mouse up at: " +
IntToStr(X) + ", " +
IntToStr(Y)); } //---------------------------------------------------------------------- |
Listing 1.10b
|
In
Listing 1.10a and 1.10b, we augment the WndProc member function in our TForm1 class. In this way, we can manipulate or customize
the way all instances of our form will respond to messages. The augmented WndProc member function definition
of Listing 1.10b contains a single swtich block, and in this case, a test for only one
message, WM_LBUTTONUP. It is in response to this
message that we call our DoMouseUp member function, in which, we extract the
mouse cursor coordinates from the LParamLo and LParamHi data members. These members correspond to the low-order and high-order words of
the 32-bit LParam data member, respectively.
IVB. Augmenting the Dispatch Member Function
In fact, augmenting the WndProc member function is not the most common way of handling a specific message. Recall, the Dispatch member function is the last function called from within the TControl::WndProc definition of Listing 1.9. This means is that after a message is processed by the TControl::WndProc member function, and previously by the TWinControl::WndProc member function for TWinControl descendants, it is passed on to the Dispatch member function. This latter function, a virtual member of the TObject base class, serves the role of either calling a specific message handler or passing on the message to the DefaultHandler member function. It then follows, that if we want to handle a specific message, we can alternatively augment the Dispatch member function. Listings 1.10c and 1.10d illustrate this idea.
//---------------------------------------------------------------------- #ifndef Unit1H #define Unit1H //---------------------------------------------------------------------- #include <Classes.hpp> #include <Controls.hpp> #include <Forms.hpp> //---------------------------------------------------------------------- class TForm1 : public TForm { private: // User
declarations void __fastcall DoMouseUp(TMessage &Msg); public: // User
declarations __fastcall TForm1(TComponent* Owner); virtual void
__fastcall Dispatch(void*
Message); }; //---------------------------------------------------------------------- extern PACKAGE TForm1 *Form1; //---------------------------------------------------------------------- #endif |
Listing 1.10c
|
//---------------------------------------------------------------------- #include <vcl.h> #pragma hdrstop #include "Unit1.h" //---------------------------------------------------------------------- #pragma resource "*.dfm" TForm1 *Form1; //---------------------------------------------------------------------- __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } //---------------------------------------------------------------------- void __fastcall TForm1::Dispatch(void* Message) { TMessage* PMsg
= static_cast<TMessage*>(Message); switch (PMsg->Msg) { case WM_LBUTTONUP: {
DoMouseUp(*PMsg); break; } }
TForm::Dispatch(Message); } //---------------------------------------------------------------------- void __fastcall TForm1::DoMouseUp(TMessage& Msg) { int X = Msg.LParamLo; int Y = Msg.LParamHi; ShowMessage("mouse up at: " +
IntToStr(X) + ", " + IntToStr(Y)); } //---------------------------------------------------------------------- |
Listing 1.10d
|
In fact, the implementation of our augmented Dispatch function in Listing 1.10d is virtually identical to that of the augmented WndProc function of Listing 1.10b. Further, both implementations are rather simple since we handle only a single message. However, as the number of messages that need be handled increases, the implementation of our switch block can become enourmous and increasingly harder to read and maintain.
IVC. Using the Message Mapping Macros
Recall, Windows itself is based on a messaging system, and as an application provides more an more functionality that relies on the Windows API, handling on the order of hundreds of messages is not uncommon. As such, the VCL introduces the the BEGIN_MESSAGE_MAP, VCL_MESSAGE_HANDLER, and END_MESSAGE_MAP macros defined in the sysdefs.h header file and provided in Listing 1.11.
#define BEGIN_MESSAGE_MAP \
virtual void __fastcall Dispatch(void *Message) \
{ \
switch
(((PMessage)Message)->Msg)
\
{ #define VCL_MESSAGE_HANDLER(msg,type,meth) \ case
msg: \ meth(*((type *)Message)); \ break; #define END_MESSAGE_MAP(base) \ default: \ base::Dispatch(Message); \ break; \ } \ } |
Listing 1.11
|
We
can see here that these macros simply perform the same process of augmenting
the Dispatch member function as in
Listing 1.10. Using the above macros,
we can rewrite the example in Listing 1.10 as that of Listing 1.12a and Listing
1.12b.
//---------------------------------------------------------------------- #ifndef Unit1H #define Unit1H //---------------------------------------------------------------------- #include <Classes.hpp> #include <Controls.hpp> #include <Forms.hpp> //---------------------------------------------------------------------- class TForm1 : public TForm { __published: // IDE-managed
Components private: // User
declarations void __fastcall DoMouseUp(TMessage& Msg); public: // User
declarations __fastcall TForm1(TComponent* Owner); BEGIN_MESSAGE_MAP
VCL_MESSAGE_HANDLER(WM_LBUTTONUP, TMessage, DoMouseUp) END_MESSAGE_MAP(TForm) }; //---------------------------------------------------------------------- extern PACKAGE TForm1 *Form1; //---------------------------------------------------------------------- #endif |
Listing 1.12a
|
//---------------------------------------------------------------------- #include <vcl.h> #pragma hdrstop #include "Unit1.h" //---------------------------------------------------------------------- #pragma resource "*.dfm" TForm1 *Form1; //---------------------------------------------------------------------- __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } //---------------------------------------------------------------------- void __fastcall TForm1::DoMouseUp(TMessage &Msg) { // extract the mouse coordinates from
the // lParam of the underlying MSG
structure int X = Msg.LParamLo; int Y = Msg.LParamHi; ShowMessage("mouse up at: " +
IntToStr(X) + ", " + IntToStr(Y)); } //---------------------------------------------------------------------- |
Listing 1.12b
|
The implementations provided in Listing 1.10 and Listing 1.12 are virtually identical. The only difference is that the latter redefines the Dispatch member function inline, implicitly though the BEGIN_MESSAGE_MAP, VCL_MESSAGE_HANDLER, and END_MESSAGE_MAP macros. The advantage presented by these macros not only lies in implementation ease, but also contributes greatly to the readability of the code and maintainability of the application.
VIII. Summary and Remarks
In this first article alone, we have covered many of the concepts that are vital in creating an event-driven application. The best advice here is not to get bogged down with the details of each implementation. A high-level picture is good enough! Let's recap the main concepts.
Beginning
with a simple "Hello World!" extension, we discussed the basis of how
an event-driven program is realized.
Particularly, we saw how the main message pump in a typical Windows
application is little more than a fancy while loop. This is the first step to understanding the
event-driven nature of most Windows applications. We also briefly discussed how the VCL's TApplication class encapsultes this main
message loop, freeing us from the neccessity of such an implemention for each
new application that we create. Next, we covered the idea of Window
messages; particularly, we discussed the difference between queued and
non-queued messages, and how the former passes through our main message pump,
while the latter is sent directly to a window procedure. We also discussed how the VCL provides an
object-oriented approach to that standard C-style window procedure, via the MakeObjectInstance VCL function and the TWinControl::WndProc member function.
To
make the link between VCL events and window messages, we examined, how the TWinControl class handles specifics
messages and fires their corresponding event handlers. We also discussed three different ways of
explcitly handling messages: augmenting the Dispatch member function (directly
and via the message mapping macros), augmenting the WndProc member function, and instance
subclassing. We ended the discussion
with an examination of the differences between each of these three approaches,
and briefly discussed how to determine which technique to implement. In
the next article of this series, we'll discuss a technique called subclassing,
which allows you to tap into the message stream of a component without creating
a new class.
Copyright © 2001 Damon Chandler; All rights reserved; e-mail: dmc27@cornell.edu