| In this chapter you take a look at the basic facts about the
BCB delegation model. Events are fundamental to the use of this programming
environment, and you will never
be able to do serious work in BCB without understanding them.
Subjects covered in this chapter include
- Using the mouse and the keyboard
- Responding directly to Windows messages such as WM_KEYDOWN or
WM_MOUSEMOVE
- Custom event structures such as TWMMouse or TMessage
- Menus and menu IDs
- Setting up a WndProc in a VCL application
- Responding to WM_COMMAND messages
This chapter is not designed to be difficult, but rather to hit the
highlights of each of these subjects so you can understand how to use events in
your own programs. By the time you are through with this chapter, you should
have a thorough understanding of the BCB delegation model and will know how to
use events in your own program. Additional material on events is included in the
chapters that cover creating components.
Events: The BCB Delegation Model
The two central ideas behind the type of RAD programming of which BCB
partakes are components and delegation. Components are treated at length in Part
IV of this book, called "Creating Components." This is where I will talk about
delegation.
Delegation is an alternative to inheritance. It's a trick to allow you to
receive the same benefits as inheritance but with less work.
Delegation is not entirely original with RAD programming; however, it is
taken much further in this paradigm than elsewhere. A part of classic windows
programming that supports delegation is the way you handle standard
WM_COMMAND messages.
Think for a moment about standard Windows programming as it appeared before
RAD rewrote the book on Windows programming. I'm talking the Windows programming
of Petzold, or of my Teach Yourself Windows 95 Programming in 21 Days (Sams
Publishing) book. Suppose that in a standard Windows program you have a button
that responds to a particular event, say a message click. You usually handled
that click inside the WM_COMMAND section of the window procedure (WndProc).
Windows was delegating the event from the button to your WM_COMMAND
handler. In other words, Windows did not force you to subclass the button
class in order to respond to clicks on a button.
This is the central idea behind the delegation model in BCB. In C++Builder,
if you drop a button on a form, you can set up an OnClick event handler
to handle clicks on the button. The great advantage this system has over the
standard Windows model is ease of use. BCB will create the message handler for
you, asking that you write only the code that responds to the event.
Specifically, it fills in the class definition with a declaration for your event
in the header:
class TForm1 : public TForm
{
__published:
TButton *Button1;
void __fastcall Button1Click(TObject *Sender); // DECLARATION HERE!
private:
public:
virtual __fastcall TForm1(TComponent* Owner);
};
And it creates the even handler itself in your CPP file:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
}
This is what is meant by the delegation model in BCB. What is radical about
the VCL's use of the delegation model is that it appears everywhere. All sorts
of components support the model and enable you to use it to do all sorts of
things that required subclassing in standard Windows programming and in OWL or
MFC.
One of the primary goals of the delegation model is to allow the main form in
your application to handle code that you would have had to use inheritance to
handle in OWL or MFC. In the inheritance model you had to override a constructor
to change the way an object was initialized. In BCB, you just respond to an
OnCreate event. If you want to modify the things a user can type into an
edit control, you don't have to subclass the control; instead, you just respond
to OnKeyDown events.
Delegation is easier than inheritance. It enables neophytes who don't
understand inheritance a way to get up to speed quickly. More importantly, it
enables programmers to get their work done quickly without a lot of repetitive
typing.
Of course, if you want to use inheritance, it is available. In fact,
inheritance is used all the time in BCB. The point is not to eliminate a
powerful technique like inheritance, but to give you an alternative to use in
the vast majority of cases where inheritance is overkill.
The Delegation Model and Contract-Free Programming
The delegation model supports something called contract-free programming. In
some semiliterate circles this has also been known as "contractless"
programming.
The idea behind contract-free programming is that there is no contract
between the developer of a component and the user of a component as to what can
and can't be done inside an event handler.
A classic example of a contract-bound program is found in Windows when you
handle WM_KILLFOCUS message. There are some things you can and cannot
do in response to this message. For example, if you change the focus during your
response to this message, you can crash Windows. There is a contract between you
and Windows not to change the focus while responding to this message. Learning
all the things you can and cannot do in response to a message is a long,
error-prone, and frustrating process.
BCB supports contract-free programming, which means you can do whatever you
want while responding to an event. BCB is religious in pursuit of this goal. In
my code, I strive to achieve this same goal, though I admit that I have been
known to cheat. When I do so, however, I severely limit the usability of my
components.
The Basics of the Delegation Model
Event-oriented code is one of the central tenets of Windows programming. Some
rapid- application development environments attempt to hide users from this
feature altogether, as if it were something so complicated that most programmers
couldn't understand it. The truth is that event-oriented programming is not, in
itself, particularly complex. However, some features of the way it is
implemented in Windows can be confusing under certain circumstances.
BCB gives you full access to the event-oriented substructure that provides
Windows with a high degree of power and flexibility. At the same time, it
simplifies and clarifies the way a programmer handles those events. The end
result is a system that gives you complete access to the power of Windows while
simultaneously protecting you from unnecessary complexity.
These next few sections cover the following topics:
- Event-oriented programming basics
- Responding to mouse events or key events
- Accessing the information passed in events
- The basics of sets, which are used frequently in BCB event handlers
- Circumventing BCB message handling tools and directly capturing messages
- Creating WM_COMMAND handlers and finding the IDs of the
components used in a program
To be really good at programming Windows, you need to be able to look at
these subjects from both the perspective of a RAD programmer and of a Windows
API programmer. That is, you need to know the VCL inside out, and you need to
know the Windows API inside out. This chapter concentrates on the VCL side of
that equation, whereas the other side is featured in books such as Teach
Yourself Windows 95 Programming in 21 Days (Sams Publishing) and Programming
Windows (Microsoft Press). Put both perspectives together and you can go on to
create your own VCL components or you can design elegant, well-structured
Windows programs.
Before starting, let me reiterate that BCB hides much of the complexity of
Windows programming. However, the developers did not want to prevent programmers
from accessing any portion of the Windows API. By the time you finish reading
this section of the chapter, you should be able to see that BCB gives you access
to the full range of power provided by an event- oriented system.
BCB Events
BCB makes it easy to handle keyboard and mouse events. Suppose, for instance,
you wanted to capture left mouse clicks in the main form of your program. Here's
how to get started. Create a new project and name it EVENTS1.MAK.
In the Object Inspector for the main form, choose the Events Page and
double-click the area to the right of the OnClick property. Create the
following function:
void __fastcall TForm1::FormClick(TObject *Sender)
{
MessageDlg("The delegation model says hello.", mtInformation,
TMsgDlgButtons() << mbOK, 0);
}
This code tells Windows that a dialog box should appear every time the user
clicks the left mouse button in the form. The dialog box is shown in Figure 4.1.
FIGURE 4.1. The dialog box displayed by the
EVENTS1 program when you click the left mouse button inside the main form.
The previous code presents one of the simplest possible cases of responding to
an event in a BCB program. It is so simple, in fact, that many programmers write
this kind of code without ever understanding that they are writing
event-oriented code. In this case, BCB programmers get the event secondhand,
because VCL massages the event before passing it on to the main form.
Nonetheless, this is real event-oriented programming, albeit in a very
simplified manifestation.
As you saw back in the section on the Windows API, the operating environment
notifies you not only of the event, but of several related bits of information.
For instance, when a mouse- down event is generated, a program is informed about
where the event occurred and which button generated the event. If you want to
get access to this kind of relatively detailed information, you should turn to
the Events Page for the form and create an OnMouseDown handler:
void __fastcall TForm1::FormMouseDown(TObject *Sender,
TMouseButton Button,
TShiftState Shift, int X, int Y)
{
if (Shift.Contains(ssRight))
{
Canvas->Brush->Style = bsClear;
Canvas->TextOut(X, Y, "* Button");
}
}
This method writes text to the form every time the user clicks the right
mouse button. It sets the brush to bsClear style to make the background
of text
transparent.
To test this method, run the program and click the right mouse button in
several different locations in the form. You'll see that each spot you click is
marked, as shown in Figure 4.2.
FIGURE 4.2. When you click the right mouse
button in the form of the EVENTS1 program, the location of the event is
recorded.
The Canvas->TextOut function prints text to the screen at the location
specified by the variables X and Y. Both of these variables
are supplied to you by BCB, which received them in turn from the operating
system. The X variable tells you the column in which the mouse was
clicked, and the Y variable tells you the row.
As you can see, BCB makes it simple for you to respond to events.
Furthermore, not only mouse events are easy to handle. You can respond to
keypresses in a similar manner. For instance, if you create a method for the
OnKeyDown property on the Events Page for the form, you can show the user
which key was pressed on the keyboard whenever the EVENTS1 program has the
focus:
void __fastcall TForm1::FormKeyDown(TObject *Sender,
WORD &Key,
TShiftState Shift)
{
MessageDlg(Key, mtInformation, TMsgDlgButtons() << mbOK, 0);
}
In the preceding code, the MessageDlg function will pop up the ASCII
value associated with a keystroke. In other words, BCB and the operating system
both pass you not an actual letter like A, B, or C,
but the number associated with the key you pressed. On PCs, the letter A
is associated with the number 65. It wouldn't be appropriate for BCB to perform
this translation for you automatically, because some keys, such as the F1 or
Enter key, have no letter associated with them. Later in this chapter you learn
how to use OnKeyDown events to respond sensibly to keypresses on
special keys such as F1, Shift, or Caps Lock.
Besides OnKeyDown events, BCB also lets you respond to keyboard
activity through the OnKeyPress event:
void __fastcall TForm1::FormKeyPress(TObject *Sender,
char &Key)
{
AnsiString S("OnKeyPress: " + AnsiString(Key));
MessageDlg(S, mtInformation, TMsgDlgButtons() << mbOK, 0);
}
You can see that this event is similar to an OnKeyDown event. The
difference is that the Key variable passed to OnKeyPress
events is already translated into a char. However, OnKeyPress
events work only for the alphanumeric keys and are not called when special keys
are pressed. In short, the OnKeyPress event is the same as a
WM_CHAR event.
The code for the EVENTS1 program is shown in Listing 4.1. Get the program up
and running and take whatever time is necessary to be sure it all makes sense to
you. There is no point in trying to be a Windows programmer if you don't
understand events.
Listing 4.1. The main form for the
EVENTS1 program.
///////////////////////////////////////
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
if (Shift.Contains(ssRight))
{
Canvas->Brush->Style = bsClear;
Canvas->TextOut(X, Y, "* Button");
}
}
void __fastcall TForm1::DelegateMe(TObject *Sender)
{
MessageDlg("The menu says hello.", mtInformation,
TMsgDlgButtons() << mbOK, 0);
}
void __fastcall TForm1::FormClick(TObject *Sender)
{
MessageDlg("The delegation model says hello.", mtInformation,
TMsgDlgButtons() << mbOK, 0);
}
void __fastcall TForm1::FormKeyPress(TObject *Sender, char &Key)
{
AnsiString S("OnKeyPress: " + AnsiString(Key));
MessageDlg(S, mtInformation, TMsgDlgButtons() << mbOK, 0);
}
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key,
TShiftState Shift)
{
MessageDlg(Key, mtInformation, TMsgDlgButtons() << mbOK, 0);
}
After this introduction to event-oriented programming, it's time to step back
and see some of the theory behind the code. After explaining something of how
the system works, this chapter goes on to give examples of how to take full
advantage of Windows event-oriented code base.
Understanding Events
Event-oriented programming isn't unique to Windows, nor is it a chore that
can be handled only by an operating system. For instance, any DOS program could
be based around a simple loop that keeps running the entire time the program is
in memory. Here is a hypothetical example of how such code might look:
do
{
CheckForMouseEvent(Events);
CheckForKeyPress(Events)
HandleEvents(Events);
} while (!Events.Done);
This code represents a typical event-oriented loop. A simple do..while
statement checks for keyboard and mouse events and then calls HandleEvents
to give the program a chance to respond to the events that are generated by the
user or the operating system.
The variable called Events might be a record with a fairly simple
structure:
struct TEvent {
int X, Y;
TButton MouseButton;
int Key;
bool Done;
};
X and Y give the current location of the cursor, and
Key contains the value of the top event in the key buffer. The TButton
type might have a declaration that looks like this:
enum TButton {ButtonLeft, ButtonRight};
These structures permit you to track where the mouse is, what state its
buttons are in, and what keys the user has pressed. Admittedly, this is a simple
type of event structure, but the principles involved mirror what is going on
inside Windows or inside other event-oriented systems such as Turbo Vision. If
the program being written was an editor, pseudo-code for the HandleEvent
for the program might look like this:
void HandleEvent(TEvent: Events)
{
switch(Events.Key)
{
case "A..z":
WriteXY(Events.X, Events.Y, Events.Key);
break;
case EnterKey:
Write(CarriageReturn);
break;
case EscapeKey:
Events.Done = TRUE;
break;
}
}
Given the preceding code, the program would go to location X,Y and
write the letter most recently pressed by the user. If the Enter key was
pressed, a carriage return would be written to the screen. A press on the Esc
key would cause the program to terminate. All other keypresses would be ignored.
Code like this can be very powerful, particularly if you're writing a program
that requires animation. For instance, if you need to move a series of bitmaps
across the screen, you want to move the bitmap a few pixels and then check to
see whether the user has pressed a button or hit a keystroke. If an event has
occurred, you want to handle it. If nothing occurred, you want to continue
moving the bitmap.
I hope the short code samples shown here give you some feeling for the way
event-oriented systems work. The only piece that's missing is an understanding
of why Windows is event- oriented.
Microsoft made Windows event-oriented in part because multiple programs run
under the environment at the same time. In multitasking systems, the operating
system needs to know whether the user has clicked in a program or whether the
click was in the desktop window. If the mouse click occurred in a window that
was partially hidden behind another window, it is up to the operating system to
recognize the event and bring that window to the foreground. Clearly, it
wouldn't be appropriate for the window itself to have to be in charge of that
task. To ask that much would place an impossible burden on the programmer who
created the window. As a result, it's best for the operating system to handle
all the keystrokes and mouse clicks, and to then pass them on to the various
programs in the form of events. Any other system would force every programmer to
handle all the events that occurred when his or her program had the focus, and
to manipulate the entire operating system in response to certain mouse events or
keystrokes, such as Alt+Tab.
In short, Windows programmers almost never directly monitor the hardware or
hardware interrupts. Instead, the operating system handles that task. It passes
all external events on to individual programs in the form of messages. In a
typical event-oriented
system, the operating system continually polls the hardware in a loop and
then sends each event off to its programs in the form of some kind of event
structure or event variables. This is the same kind of activity you saw in the
brief code snippets shown earlier.
You have seen that Windows handles mouse and keyboard events, and passes them
on to the appropriate window. The message that is generated in these cases gets
sent to the default window function, which, as you know, is called
DefWindowProc. DefWindowProc is analogous to the HandleEvent
function shown earlier.
The important point to understand is that Windows messages contain the
information that drives the entire operating environment. Almost everything that
happens inside Windows is a message; if you really want to tap into the power of
BCB, you need to understand how these messages work.
One of the tricky parts of Windows event-oriented programming is extracting
information
from the WPARAM and LPARAM variables passed to the window
function. In most cases, BCB frees you from the necessity of performing this
task. For instance, if you create an event for the OnMouseDown
property, BCB directly tells you the X value and Y value where
the event occurred. As a programmer, you don't have to struggle to get the event
and its associated values. As you will see in the next section, everything about
the event is shown to you in a simple and straightforward manner.
-
NOTE: WPARAM and
LPARAM are the types of the parameters passed to a standard WndProc
as is declared in WinUser.h:
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT,
WPARAM, LPARAM);
These variables are usually given names such as wParam and
lParam or WParam and LParam:
WndProc(HWND hWnd, UINT Message, WPARAM wParam,
LPARAM lParam);
You will find that I sometimes refer to these parameters by type in all
caps, and sometimes as variable identifiers with mixed caps and small letters.
In all cases, I am referring to the same variables passed to the user from the
system. Even in the discussion of the TMessage type, which occurs
later in the chapter, I am still referring to these same parameters, only in a
slightly different context. Specifically, TMessage is a VCL type that
contains exact copies of these parameters. The X and Y
parameters passed to an OnMouseDown event are not exact copies of
WPARAM and LPARAM, but variables designed to display information
originally contained in WPARAM or LPARAM after they have
been parsed by the VCL.
Using Sets to Track Messages
Rather than ask you to parse the LPARAM and WPARAM
parameters, BCB performs this chore for you and then passes the information on
in the form of parameters:
void __fastcall TForm1::FormMouseDown(
TObject *Sender,
TMouseButton Button,
TShiftState Shift,
int X,
int Y)
This is by far the most convenient way for you to handle events. BCB can also
give direct access to the values sent to you by the operating system. That is,
you can handle WPARAM and LPARAM directly if you want. After
you have studied the EVENTS2 program and learned more about sets, I'll show you
exactly how to get at that raw data.
Take a moment to consider the Shift parameter shown in the
FormMouseDown header. Shift is declared to be of type
TShiftState:
enum Classes_1 { ssShift, ssAlt, ssCtrl, ssLeft,
ssRight, ssMiddle, ssDouble };
typedef Set<Classes_1, ssShift, ssDouble> TShiftState;
TShiftState is a set, that is, it's an instance of the Set
template class from SYSDEFS.H. To find out whether a particular element
is a member of the set passed to you by BCB, you can perform simple tests using
the Contains method:
ShiftKey->Checked = Shift.Contains(ssShift);
ControlKey->Checked = Shift.Contains(ssCtrl);
LeftButton->Checked = Shift.Contains(ssLeft);
RightButton->Checked = Shift.Contains(ssRight);
This code asks whether the element ssRight is in the set passed to
you via the Shift variable. If it is, the code sets the state of a
TCheckBox component.
Here is how you can declare a set at runtime:
TShiftState LeftShift;
LeftShift << ssLeft << ssShift;
Given this set, the Contains operator returns True if you
ask about ssLeft or ssShift.
Besides the important Contains method, there are three key operators
you can use with the Set class:
+ Union
- Difference
* Intersection
All three of these operators return a set, whereas Contains returns
a Boolean value. The SETSEXP program, shown in Listing 4.2, shows how to work
with these key elements of the Set template class.
Listing 4.2. The SETEXP program shows how to use
operators to track the members of sets such as TShiftState.
///////////////////////////////////////
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void TForm1::CheckState(TShiftState Shift)
{
ShiftKey->Checked = Shift.Contains(ssShift);
ControlKey->Checked = Shift.Contains(ssCtrl);
LeftButton->Checked = Shift.Contains(ssLeft);
RightButton->Checked = Shift.Contains(ssRight);
}
void __fastcall TForm1::UnionClick(TObject *Sender)
{
AnsiString Operators[3] = {"+", "*", "-"};
TShiftState FinalSet;
TShiftState LeftShift;
TShiftState LeftCtrl;
LeftShift << ssLeft << ssShift;
LeftCtrl << ssLeft << ssCtrl;
switch (TOptType(dynamic_cast <TBitBtn*>(Sender)->Tag))
{
case otUnion:
FinalSet = LeftShift + LeftCtrl;
break;
case otIntersection:
FinalSet = LeftShift * LeftCtrl;
break;
case otDifference:
FinalSet = LeftShift - LeftCtrl;
break;
}
CheckState(FinalSet);
Label2->Caption = Operators[dynamic_cast<TBitBtn *>(Sender)->Tag];
}
void __fastcall TForm1::Label4MouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
CheckState(Shift);
}
The SETEXP program shows how you can read and manipulate sets. In particular,
you work with sets of type TShiftState. When you are finished studying
the program, you will have all the knowledge you need to work with BCB sets.
The main form for the SETEXP program consists of four checkboxes, two panels,
four labels, and three bitbtns, as shown in Figure 4.3. The four checkboxes are
placed on top of the first panel, and the fourth label is placed on the top of
the second panel.
FIGURE 4.3. The SETEXP program's main form
enables you to manipulate variables of type TShiftState.
SETEXP tells you whether the Shift or Ctrl keys are pressed when the mouse is
clicked, and it tells you whether the user pressed the right or left mouse
button. The code also shows how to use the Intersection, Union,
and Difference operators.
The key method in the SETEXP program looks at a variable of type
TShiftState and displays its contents to the user through the program's
radio buttons:
void TForm1::CheckState(TShiftState Shift)
{
ShiftKey->Checked = Shift.Contains(ssShift);
ControlKey->Checked = Shift.Contains(ssCtrl);
LeftButton->Checked = Shift.Contains(ssLeft);
RightButton->Checked = Shift.Contains(ssRight);
}
This code takes advantage of the fact that Contains returns a
Boolean variable, and the Checked property of a radio button is also
declared to be of type Boolean. As a result, you can test to see whether a
particular element is part of the Shift set. If it is, you can easily
set the Checked state of a radio button to record the result. For
example, in the preceding code, if ssShift is part of the current set,
the Shift Key radio button is checked.
Two different routines pass variables of type TShiftState to the
CheckState method. The first routine is called whenever the user clicks in
the panel at the bottom of the program or the label that rests on the panel:
void __fastcall TForm1::Label4MouseDown(TObject
*Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
CheckState(Shift);
}
This code passes the Shift variable on to CheckState, which
displays the contents of the variable to the user. For instance, if the Shift
key is being held down and the right mouse button is pressed,
Label4MouseDown is called. Label4MouseDown then passes the
Shift variable to CheckState, and CheckState causes the
ShiftKey and RightButton controls to be checked. The other two
radio buttons are left unchecked.
There are three bitbtns on the right side of the main form. They are labeled
Union, Intersection, and Difference. Clicking any of these buttons demonstrates
one of the non-Boolean set operators. All three buttons have their OnClick
event set to the following function:
void __fastcall TForm1::UnionClick(TObject *Sender)
{
AnsiString Operators[3] = {"+", "*", "-"};
TShiftState FinalSet;
TShiftState LeftShift;
TShiftState LeftCtrl;
LeftShift << ssLeft << ssShift;
LeftCtrl << ssLeft << ssCtrl;
switch (TOptType(dynamic_cast <TBitBtn*>(Sender)->Tag))
{
case otUnion:
FinalSet = LeftShift + LeftCtrl;
break;
case otIntersection:
FinalSet = LeftShift * LeftCtrl;
break;
case otDifference:
FinalSet = LeftShift - LeftCtrl;
break;
}
CheckState(FinalSet);
Label2->Caption = Operators[dynamic_cast<TBitBtn *>(Sender)->Tag];
}
The UnionClick method declares three variables of type
TShiftState. Two of these variables are used to create sets that are used
by the rest of the SetBtnClick method:
LeftShift << ssLeft << ssShift;
LeftCtrl << ssLeft << ssCtrl;
The first line assigns the LeftShift variable to a set that contains
the values ssLeft and ssShift. The next line assigns the
LeftCtrl variable to a set that contains ssLeft and ssCtrl.
The rest of this method enables the user to see the union, intersection, and
difference of these two sets.
The switch statement in the middle of the SetBtnClick
method detects which of the three bitbtns the user clicked. This is the old,
time-honored technique featuring the use of an enumerated type and the
assignment of zero-based ordinal values to the Tag field of each
button.
If the user clicks the Union button, the FinalSet variable is set to
the union of the LeftShift and LeftCtrl variables:
FinalSet = LeftShift + LeftCtrl;
A click on the Intersection button executes the following code:
FinalSet = LeftShift * LeftCtrl;
The difference of the sets is calculated if the user clicks the Difference
button:
FinalSet = LeftShift - LeftCtrl;
After the switch statement ensures the selection of the proper
operator, the FinalSet value is passed to CheckState and its
contents are displayed to the user. For instance, if the user clicks the Union
button, the LeftButton, ShiftKey, and ControlKey radio buttons are all checked.
The Intersection button causes the LeftKey to be checked, and the Difference
button causes the ShiftKey to be checked. Here is another way of looking at the
work accomplished by these operators:
[ssLeft, ssShift] + [ssLeft, ssCtrl] = [ssLeft,
ssShift, ssCtrl];
[ssLeft, ssShift] * [ssLeft, ssCtrl] = [ssLeft]
[ssLeft, ssShift] - [ssLeft, ssCtrl] = [ssShift]
To help the user understand exactly what is happening, the current set
operation is displayed at the top of the form. For instance, if the user clicks
the Union button, the following expression is shown to the user:
LeftShift + LeftCtrl
Throughout a run of the program, the words LeftShift and
LeftCtrl are displayed to the user in a pair of TLabels. A third
label displays +, -, or *, depending on the current
state of the program:
Label2->Caption = Operators[dynamic_cast<TBitBtn
*>(Sender)->Tag];
In this code, Operators is an array of three strings that contains
the operators that return a set:
AnsiString Operators[3] = {"+", "*", "-"};
The SETEXP program gives you enough information that you should be able to
work
with the sets that are passed to BCB event handlers. The code shown here
defines the way sets are usually handled in all BCB programs. However, you can
actually directly manipulate the raw data that represents a BCB set. Techniques
for performing these manipulations are shown in the GetShift method,
which is part of the program examined in the next section of this chapter. You
can also review the information on sets in Chapter 3.
Tracking the Mouse and Keyboard
You now know enough to begin an in-depth study of the main event handlers
used by BCB forms and controls. The EVENTS2 program, shown in Listings 4.3
through 4.5, enables you to trace the occurrence of all the keyboard or mouse
interrupts generated during the run of a program.
Listing 4.3. The EVENTS2 header and main form provide a
detailed look at how to track events.
//----------------------------------
#ifndef MainH
#define MainH
//----------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ExtCtrls.hpp>
//----------------------------------
class TForm1 : public TForm
{
__published:
TPanel *Panel1;
TLabel *Label1;
TLabel *LMouseMove;
TLabel *Label3;
TLabel *LMouseDown;
TLabel *Label5;
TLabel *LKeyDown;
TLabel *Label7;
TLabel *LKeyUp;
TLabel *Label9;
TLabel *LMouseUp;
TLabel *Label11;
TLabel *LWidth;
TLabel *Label13;
TLabel *LHeight;
TLabel *LSpecialMouse;
TLabel *Label16;
void __fastcall FormResize(TObject *Sender);
void __fastcall FormPaint(TObject *Sender);
void __fastcall FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormKeyUp(TObject *Sender, WORD &Key, TShiftState Shift);
void __fastcall FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift);
void __fastcall FormMouseMove(TObject *Sender, TShiftState Shift, int X,
int Y);
void __fastcall FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
private:
MESSAGE void MyMouseMove(TWMMouse &Message);
public:
virtual __fastcall TForm1(TComponent* Owner);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_MOUSEMOVE, TWMMouse, MyMouseMove);
END_MESSAGE_MAP(TForm);
};
//----------------------------------
extern TForm1 *Form1;
//----------------------------------
#endif
Listing 4.4. The EVENTS2 main form provides a
detailed look at how to track events.
///////////////////////////////////////
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl.h>
#pragma hdrstop
#include "Main.h"
#include "VKeys1.h"
#include "Binary.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void TForm1::MyMouseMove(TWMMouse &Message)
{
TForm::Dispatch(&Message);
LSpecialMouse->Caption = "X " + IntToStr(Message.XPos) +
" Y " + IntToStr(Message.YPos);
}
void __fastcall TForm1::FormResize(TObject *Sender)
{
LHeight->Caption = IntToStr(Width);
LWidth->Caption = IntToStr(Height);
}
void __fastcall TForm1::FormPaint(TObject *Sender)
{
Canvas->Font->Name = "New Times Roman";
Canvas->Font->Size = 48;
Canvas->TextOut(1, Panel1->Height, "Mouse Zone");
}
void __fastcall TForm1::FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
LMouseUp->Caption = "X " + IntToStr(X) + " Y " +
IntToStr(Y) + " " + GetShift(Shift);
}
void __fastcall TForm1::FormKeyUp(TObject *Sender, WORD &Key, TShiftState
Shift)
{
LKeyUp->Caption = GetKey(Key) + " " + GetShift(Shift);
}
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key,
TShiftState Shift)
{
LKeyDown->Caption = GetKey(Key) + " " + GetShift(Shift);
}
void __fastcall TForm1::FormMouseMove(TObject *Sender, TShiftState Shift, int
X,
int Y)
{
LMouseMove->Caption = "X " + IntToStr(X) + " Y " +
IntToStr(Y) + " " + GetShift(Shift);
}
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
LMouseDown->Caption = GetShift(Shift);
}
Listing 4.5. The VKeys unit is used by the EVENTS2
program.
///////////////////////////////////////
// Vkeys.cpp
// Project Name: Events2
// Copyright (c) 1997 by Charles Calvert
//
#include <vcl.h>
#pragma hdrstop
#include "VKeys1.h"
AnsiString MessageArray[5] =
{"WM_CHAR", "WM_KEY", "WM_MOUSEMOVE", "WM_MOUSEDOWN", "WM_MOUSEUP"};
AnsiString ButtonArray[3] =
{"mbLeft", "mbRight", "mbCenter"};
AnsiString ShiftArray[8] = {"ssShift", "ssAlt", "ssCtrl", "ssLeft",
"ssRight", "ssMiddle", "ssDouble", "ssUnknown"};
AnsiString GetShift(TShiftState State)
{
int B = 0, i;
AnsiString S;
for (i = 0; i <= 7; i++)
{
if (State.Contains(i))
S = S + " " + ShiftArray[i];
}
return S;
}
AnsiString GetKey(WORD K)
{
AnsiString S;
switch (K)
{
case VK_LBUTTON: S = "VK_LButton"; break;
case VK_RBUTTON : S = "VK_RBUTTON"; break;
case VK_CANCEL : S = "VK_CANCEL"; break;
case VK_MBUTTON : S = "VK_MBUTTON"; break;
case VK_BACK : S = "VK_BACK"; break;
case VK_TAB : S = "VK_TAB"; break;
case VK_CLEAR : S = "VK_CLEAR"; break;
case VK_RETURN : S = "VK_RETURN"; break;
case VK_SHIFT : S = "VK_SHIFT"; break;
case VK_CONTROL : S = "VK_CONTROL"; break;
case VK_MENU : S = "VK_MENU"; break;
case VK_PAUSE : S = "VK_PAUSE"; break;
case VK_CAPITAL : S = "VK_CAPITAL"; break;
case VK_ESCAPE : S = "VK_ESCAPE"; break;
case VK_SPACE : S = "VK_SPACE"; break;
case VK_PRIOR : S = "VK_PRIOR"; break;
case VK_NEXT : S = "VK_NEXT"; break;
case VK_END : S = "VK_END"; break;
case VK_HOME : S = "VK_HOME"; break;
case VK_LEFT : S = "VK_LEFT"; break;
case VK_UP : S = "VK_UP"; break;
case VK_RIGHT : S = "VK_RIGHT"; break;
case VK_DOWN : S = "VK_DOWN"; break;
case VK_SELECT : S = "VK_SELECT"; break;
case VK_PRINT : S = "VK_PRINT"; break;
case VK_EXECUTE : S = "VK_EXECUTE"; break;
case VK_SNAPSHOT : S = "VK_SNAPSHOT"; break;
case VK_INSERT : S = "VK_INSERT"; break;
case VK_DELETE : S = "VK_DELETE"; break;
case VK_HELP : S = "VK_HELP"; break;
case VK_NUMPAD0 : S = "VK_NUMPAD0"; break;
case VK_NUMPAD1 : S = "VK_NUMPAD1"; break;
case VK_NUMPAD2 : S = "VK_NUMPAD2"; break;
case VK_NUMPAD3 : S = "VK_NUMPAD3"; break;
case VK_NUMPAD4 : S = "VK_NUMPAD4"; break;
case VK_NUMPAD5 : S = "VK_NUMPAD5"; break;
case VK_NUMPAD6 : S = "VK_NUMPAD6"; break;
case VK_NUMPAD7 : S = "VK_NUMPAD7"; break;
case VK_NUMPAD8 : S = "VK_NUMPAD8"; break;
case VK_NUMPAD9 : S = "VK_NUMPAD9"; break;
case VK_MULTIPLY : S = "VK_MULTIPLY"; break;
case VK_ADD : S = "VK_ADD"; break;
case VK_SEPARATOR : S = "VK_SEPARATOR"; break;
case VK_SUBTRACT : S = "VK_SUBTRACT"; break;
case VK_DECIMAL : S = "VK_DECIMAL"; break;
case VK_DIVIDE : S = "VK_DIVIDE"; break;
case VK_F1 : S = "VK_F1"; break;
case VK_F2 : S = "VK_F2"; break;
case VK_F3 : S = "VK_F3"; break;
case VK_F4 : S = "VK_F4"; break;
case VK_F5 : S = "VK_F5"; break;
case VK_F6 : S = "VK_F6"; break;
case VK_F7 : S = "VK_F7"; break;
case VK_F8 : S = "VK_F8"; break;
case VK_F9 : S = "VK_F9"; break;
case VK_F10 : S = "VK_F10"; break;
case VK_F11 : S = "VK_F11"; break;
case VK_F12 : S = "VK_F12"; break;
case VK_F13 : S = "VK_F13"; break;
case VK_F14 : S = "VK_F14"; break;
case VK_F15 : S = "VK_F15"; break;
case VK_F16 : S = "VK_F16"; break;
case VK_F17 : S = "VK_F17"; break;
case VK_F18 : S = "VK_F18"; break;
case VK_F19 : S = "VK_F19"; break;
case VK_F20 : S = "VK_F20"; break;
case VK_F21 : S = "VK_F21"; break;
case VK_F22 : S = "VK_F22"; break;
case VK_F23 : S = "VK_F23"; break;
case VK_F24 : S = "VK_F24"; break;
case VK_NUMLOCK : S = "VK_NUMLOCK"; break;
case VK_SCROLL : S = "VK_SCROLL"; break;
default:
S = K;
}
return S;
}
EVENTS2 shows how to extract the full content of a message sent to you by
BCB. The main form for the program (shown in Figure 4.4) provides information on
a wide range of mouse and keyboard-generated events.
FIGURE 4.4. The EVENTS2 program tracks key
Windows events as they occur.
To use the program, simply compile and run it. Click the mouse in random
locations and strike any of the keys on the keyboard. Just rattle away; you
won't do any harm unless you press Ctrl+Alt+Del. Every time you move the mouse,
click the mouse or strike a key; the exact nature of the event that occurred is
shown in the main form of the program. For instance, if you move the mouse, its
current location is shown on the form. If you press the F1 key while the Ctrl
key is pressed, those keys' values are displayed on the form.
If you don't have the MyMouseMove function defined, the
FormMouseMove event handler in the EVENTS2 window tracks the
current location of the mouse and the state of its buttons. It does this by
responding to OnMouseMove events:
void __fastcall TForm1::FormMouseMove(TObject *Sender,
TShiftState Shift, int X,
int Y)
{
LMouseMove->Caption = "X " + IntToStr(X) + " Y " +
IntToStr(Y) + " " + GetShift(Shift);
}
The method for tracking the X and Y values is fairly
intuitive.
X stands for the current column, and Y stands for the
current row, with columns and rows measured in pixels. Before these values can
be shown to the user, they need to be translated into strings by the
IntToStr function.
Nothing could be simpler than the techniques used to record the current
location of the mouse.
The technique for recording the current shift state, however, is a bit more
complex. As you saw earlier, the elements of this set track all the possible
states of the Shift, Alt, and Ctrl keys, as well as the mouse buttons.
The Set class makes it a simple matter to create a function that
will find all the currently selected elements of a variable of type
TShiftState:
AnsiString GetShift(TShiftState State)
{
int B = 0, i;
AnsiString S;
for (i = 0; i <= 7; i++)
{
if (State.Contains(i))
S = S + " " + ShiftArray[i];
}
return S;
}
The preceding code takes advantage of the following constant array:
AnsiString ShiftArray[8] = {"ssShift", "ssAlt",
"ssCtrl", "ssLeft",
"ssRight", "ssMiddle", "ssDouble", "ssUnknown"};
More specifically, the code checks to see whether the first possible element
of the set is active, and if it is, "ssShift" is added to the string
returned by the function. If the next element in the enumerated type underlying
the set is present, the string "ssAlt" is added to the string returned
by the function, and so on. As you move through the elements in the underlying
enumerated type, you get a picture of the current state of the mouse and
keyboard. For instance, if the Shift and Ctrl keys are pressed, as well as the
right mouse button, the string returned by the function looks like this:
ssShift ssCtrl ssRight
Trapping Virtual Keys
When keys are pressed in a Windows program, two different messages can be
sent to your program. One message is called WM_KEYDOWN, and it is sent
whenever any key on the keyboard is pressed. The second message is called
WM_CHAR, and it is sent when one of the alphanumeric keys is pressed. In
other words, if you press the A key, you get both a WM_KEYDOW
and a WM_CHAR message. If you press the F1 key, only the WM_KEYDOWN
message is sent.
OnKeyPress event handlers correspond to WM_CHAR messages,
and OnKeyDown events correspond to WM_KEYDOWN events. That's
why OnKeyPress handlers are passed a Key variable that is of
type char, and OnKeyDown handlers are passed a Key
variable that is of type WORD.
When you get a WM_KEYDOWN message, you need to have some way of
translating that message into a meaningful value. To help with this chore,
Windows declares a set of virtual key constants that all start with vk.
For example, if you press the F1 key, the Key variable passed to an
OnKeyDown event is set to VK_F1, in which the letters vk
stand for virtual key. The virtual key codes are found in the WINDOWS
unit and also in the online help under Virtual Key Codes.
You can test to see which virtual key has been pressed by writing code that
looks like this:
if (Key == VK_CANCEL)
DoSomething();
This code simply tests to see whether a particular key has been pressed. If
it has, the code calls the DoSomething function.
To help you understand virtual keys, the GetKey method from the
VKEYS unit returns a string stating exactly what key has been pressed:
AnsiString GetKey(WORD K)
{
AnsiString S;
switch (K)
{
case VK_LBUTTON: S = "VK_LButton"; break;
case VK_RBUTTON : S = "VK_RBUTTON"; break;
case VK_CANCEL : S = "VK_CANCEL"; break;
case VK_MBUTTON : S = "VK_MBUTTON"; break;
case VK_BACK : S = "VK_BACK"; break;
case VK_TAB : S = "VK_TAB"; break;
case VK_CLEAR : S = "VK_CLEAR"; break;
case VK_RETURN : S = "VK_RETURN"; break;
case VK_SHIFT : S = "VK_SHIFT"; break;
case VK_CONTROL : S = "VK_CONTROL"; break;
case VK_MENU : S = "VK_MENU"; break;
case VK_PAUSE : S = "VK_PAUSE"; break;
case VK_CAPITAL : S = "VK_CAPITAL"; break;
case VK_ESCAPE : S = "VK_ESCAPE"; break;
case VK_SPACE : S = "VK_SPACE"; break;
case VK_PRIOR : S = "VK_PRIOR"; break;
case VK_NEXT : S = "VK_NEXT"; break;
case VK_END : S = "VK_END"; break;
case VK_HOME : S = "VK_HOME"; break;
case VK_LEFT : S = "VK_LEFT"; break;
case VK_UP : S = "VK_UP"; break;
case VK_RIGHT : S = "VK_RIGHT"; break;
case VK_DOWN : S = "VK_DOWN"; break;
case VK_SELECT : S = "VK_SELECT"; break;
case VK_PRINT : S = "VK_PRINT"; break;
case VK_EXECUTE : S = "VK_EXECUTE"; break;
case VK_SNAPSHOT : S = "VK_SNAPSHOT"; break;
case VK_INSERT : S = "VK_INSERT"; break;
case VK_DELETE : S = "VK_DELETE"; break;
case VK_HELP : S = "VK_HELP"; break;
case VK_NUMPAD0 : S = "VK_NUMPAD0"; break;
case VK_NUMPAD1 : S = "VK_NUMPAD1"; break;
case VK_NUMPAD2 : S = "VK_NUMPAD2"; break;
case VK_NUMPAD3 : S = "VK_NUMPAD3"; break;
case VK_NUMPAD4 : S = "VK_NUMPAD4"; break;
case VK_NUMPAD5 : S = "VK_NUMPAD5"; break;
case VK_NUMPAD6 : S = "VK_NUMPAD6"; break;
case VK_NUMPAD7 : S = "VK_NUMPAD7"; break;
case VK_NUMPAD8 : S = "VK_NUMPAD8"; break;
case VK_NUMPAD9 : S = "VK_NUMPAD9"; break;
case VK_MULTIPLY : S = "VK_MULTIPLY"; break;
case VK_ADD : S = "VK_vkADD"; break;
case VK_SEPARATOR : S = "VK_SEPARATOR"; break;
case VK_SUBTRACT : S = "VK_SUBTRACT"; break;
case VK_DECIMAL : S = "VK_DECIMAL"; break;
case VK_DIVIDE : S = "VK_DIVIDE"; break;
case VK_F1 : S = "VK_F1"; break;
case VK_F2 : S = "VK_F2"; break;
case VK_F3 : S = "VK_F3"; break;
case VK_F4 : S = "VK_F4"; break;
case VK_F5 : S = "VK_F5"; break;
case VK_F6 : S = "VK_F6"; break;
case VK_F7 : S = "VK_F7"; break;
case VK_F8 : S = "VK_F8"; break;
case VK_F9 : S = "VK_F9"; break;
case VK_F10 : S = "VK_F10"; break;
case VK_F11 : S = "VK_F11"; break;
case VK_F12 : S = "VK_F12"; break;
case VK_F13 : S = "VK_F13"; break;
case VK_F14 : S = "VK_F14"; break;
case VK_F15 : S = "VK_F15"; break;
case VK_F16 : S = "VK_F16"; break;
case VK_F17 : S = "VK_F17"; break;
case VK_F18 : S = "VK_F18"; break;
case VK_F19 : S = "VK_F19"; break;
case VK_F20 : S = "VK_F20"; break;
case VK_F21 : S = "VK_F21"; break;
case VK_F22 : S = "VK_F22"; break;
case VK_F23 : S = "VK_F23"; break;
case VK_F24 : S = "VK_F24"; break;
case VK_NUMLOCK : S = "VK_NUMLOCK"; break;
case VK_SCROLL : S = "VK_SCROLL"; break;
default:
S = char(K);
}
return S;
}
This function is really just a giant case statement that checks to
see whether the Key variable is equal to any of the virtual keys. If it
is not, the code assumes that it must be one of the standard keys between A
and Z. (See the else clause in the code to see how these
standard keys are handled.)
As explained in the last paragraph, the virtual key codes do not cover normal
letters such as A, B, and C. In other words, there is
no value VK_A or VK_B. To test for these letters, just use the
standard ASCII values. In other words, test whether Key is equal to
65, or whether char(key) = `A'. The point here is that these
letters already have key codes. That is, the key codes for these letters are the
literal values A, B, C, and so on. Because these are
perfectly serviceable values, there is no need to create virtual key codes for
the standard letters of the alphabet, or for numbers.
To see the value as a number, make the default section of the code look like
this:
S = K;
Then if you press the A key, you get back 65. If you want to see the
letter `A' instead, write the following:
S = char(K);
You probably won't have much use for the GetKey routine in a
standard BCB program. However, it is useful when you are trying to understand
virtual keys and the OnKeyDown event. As a result, I have included it
in this program.
Handling Events Directly
If you look at the bottom of the EVENTS2 form, you see that there is
a special event that tracks the position of the mouse. The EVENTS2 program
tracks the mouse movements in two different ways because I wanted to show you
that you can get information about the mouse either by responding to
OnMouseMove events or by directly tracking WM_MOUSEMOVE messages.
Here is how you declare a function that is going to directly capture a
message:
class TForm1 : public TForm
{
__published:
... // Declarations omitted
private:
MESSAGE void MyMouseMove(TWMMouse &Message);
public:
virtual __fastcall TForm1(TComponent* Owner);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_MOUSEMOVE, TWMMouse, MyMouseMove);
END_MESSAGE_MAP(TForm);
};
The declaration shown here tells BCB that you want to respond directly when
the operating system informs your program that the mouse has moved. In other
words, you don't want the BCB VCL to trap the message first and then pass it on
to you in an
OnMouseMove event. Instead, you just want the message sent straight
to you by the operating system, as if you were working with one of the Windows
API programs shown earlier in the book. In short, you're telling the VCL: "Yes,
I know you
can make this task very simple and can automate nearly the entire process by
using visual tools. That's nice of you, but right now I want to get the real
event itself. I have some reason of my own for wanting to get very close to the
metal. As a result, I'm going to grab the message before you ever get a chance
to look at it!"
Here's the code for the MyMouseMove function:
void TForm1::MyMouseMove(TWMMouse &Message)
{
TForm::Dispatch(&Message);
LSpecialMouse->Caption = "X " + IntToStr(Message.XPos) +
" Y " + IntToStr(Message.YPos);
}
You can see that the code begins by calling the Dispatch method
inherited from TObject. If you didn't make this call, the program would
still run, but the OnMouseMove event would never be sent to the
FormMouseMove function. It isn't an error if you don't pass the message
back to BCB. You can either keep the message for yourself or pass it on, as you
prefer.
If you omit the call to Dispatch from the MySpecialMouse
function, the FormMouseMove method in the EVENTS2 program is no longer
called. In other words, you are directly trapping WM_MOUSEMOVE messages
and not passing them on to the VCL. As a result, the VCL does not know that the
event occurred, and FormMouseMove is not called.
The explanation in the last paragraph might not be easy to grasp unless you
actually experiment with the EVENTS2 program. You should run the program once
with the default version of the MyMouseMove method, and once with the
call to Inherited commented out:
void TForm1::MyMouseMove(TWMMouse &Message)
{
// TForm::Dispatch(&Message);
LSpecialMouse->Caption = "X " + IntToStr(Message.XPos) +
" Y " + IntToStr(Message.YPos);
}
Notice that when you run the program this way, the OnMouseMove
message at the top of the form is left blank.
If you look at the header for the MyMouseMove function, you can see
that
it is passed a parameter of type TWMMouse. As you recall, the
TWMMouse record, found in MESSAGES.PAS, looks like this:
struct TWMMouse
{
unsigned int Msg;
long Keys;
union
{
struct
{
Windows::TSmallPoint Pos;
long Result;
};
struct
{
short XPos;
short YPos;
};
};
};
If you break out both of the options shown in this variant record, you can
further
simplify this record by writing
struct TWMMouse
{
unsigned int Msg;
long Keys;
Windows::TSmallPoint Pos;
long Result;
};
or
struct TWMMouse
{
unsigned int Msg;
long Keys;
short XPos;
short YPos;
};
For most users, one of these two views will be the most useful way to picture
the record.
The same information is present in a TWMMouse record that you would
find if you responded to an OnMouseMove or OnMouseDown event.
If appropriate, you can find out the row and column where the mouse is located,
what key is pressed, and what state the Shift, Alt, and Ctrl keys are in. To
pursue this matter further, you should look up WM_MOUSEMOVE and
WM_MOUSEDOWN messages in the online help.
TWMMouse plays the same role in a BCB program that message crackers
from WINDOWSX.H play in a C++ program. In other words, they
automatically break out the values passed in lParam or wParam
parameters of the WndProc. However, if you want, you can pass a
variable of type TMessage as the parameter sent to the WM_MOUSEMOVE
message handler.
Because TMessage and TWMMouse are both the same size, BCB
doesn't care which one you use when trapping WM_MOUSEMOVE events. It's
up to you to decide how you want to crack the wParam and lParam
parameters passed to the WndProc.
In this section, you have learned something about directly handling Windows
messages. When you write code that captures messages directly, you are in a
sense reverting back to the more complicated model of programming found in
Borland C++ 5.x. However, there are times when it is helpful to get close to the
machine; BCB lets you get there if that is what you need to do.
Menu IDs, Handling WM_COMMAND, Finding TForms WndProc
In standard Windows programming, as it was conducted before the appearance of
visual tools, one of the most important messages was WM_COMMAND. This
message
was sent to a program every time the user selected a menu item or a button,
or clicked almost any other control that is part of the current program.
Furthermore, each of the buttons, menu items, and other controls in a program
had a special ID, which
was assigned by the programmer. This ID was passed to WM_COMMAND
handlers in the wParam variable.
BCB handles WM_COMMAND messages in such a way that you almost never
have to think about them. For instance, you can get clicks on a button or menu
by using the delegation model. Standard BCB controls still have IDs, but BCB
assigns these numbers automatically, and there is no obvious way for you to
learn the value of these IDs.
Despite BCB's capability to simplify this aspect of Windows programming,
there are still times when you want to get down to the bare bones and start
handling WM_COMMAND messages yourself. In particular, you will want to
find a way to discover the ID associated with a particular command, and you will
want to trap that ID inside a WM_COMMAND handler.
-
WARNING: If you are new to good
RAD programming tools like BCB, you are likely to think you need to get hold
of menu IDs a lot more often than you really need to get hold of them. When I
first came to this paradigm, I was used to using menu IDs and I had a lot of
tricks that were associated with them. As a result, I was determined to use
them in my first RAD programs. That was a mistake, and I did nothing but waste
time trying to walk down that road.
If the developers of the VCL thought you had any need at all for these ids,
they would have made them readily available. The fact that they are a bit
difficult to get at should tell you that it is very rare for anyone to ever
need to access these items. It wouldn't have been hard to make these IDs
available; there was simply no need to do it, and some good reasons to hint
broadly that RAD had some better ways to do this type of programming. The
obscurity of menu IDs is not a weakness of RAD; they are just a broad hint
that you don't need to worry about that side of Windows programming any more.
The people who designed the VCL are very bright folks who know the Windows API
in intimate, excruciating detail. Their goal was to create an easy-to-use
programming environment that still gives users full access to all the features
of Windows. They would never deliberately cut you off from something you
needed.
Don't forget that both Delphi and BCB are built in the VCL by the same people
who created the VCL in the first place. One of the reasons the VCL is so good
is that it was honed by its creators during the process of creating the Delphi
and BCB IDEs. If there were something wrong with the way the VCL was designed,
it became apparent to the developers while they were creating the IDE, and it
was fixed.
This was not a tool created cynically in a back room by a bunch of guys who
looked down on their own users as second-rate programmers. They themselves
were the programmers who first used their tools, so they did everything they
could to build them correctly.
The MENUDEF program gives a general overview of how to handle
WM_COMMAND messages. The program enables you to discover the ID used by a
series of menu items
and then enables you to trap these IDs when they are sent to a WM_COMMAND
handler in the form of a TMessage.wParam variable.
As a bonus, the program also shows how to use the TForm WndProc
method, which lets you set up your own window function that receives all the
messages sent to your form. It is extremely rare that you would ever need to
override this virtual method in your own programs, but I show you how to do it
just so you will understand a little more about how the VCL operates. You should
be warned that this is a dangerous method to override, because it might come
into existence before your controls are initialized and ready for use and might
disappear after the controls have been disposed. For instance, I check for
WM_CLOSE messages, and once I get one, I don't try to show you any more
messages coming into the WndProc. Needless to say, you would crash your
program immediately if you overrode this method and did not call its ancestor.
Figure 4.5 shows the form for the MENUDEF program. Here are the menu items
that you can't see in Figure 4.5:
Caption = `File'
Caption = `Open'
Caption = `Close'
Caption = `Exit'
Caption = `Edit'
Caption = `Cut'
Caption = `Copy'
Caption = `Paste'
FIGURE 4.5. The MENUDEF program uses a
TMemo, a TButton, and a TMainMenu control.
The code for the MENUDEF program is in Listings 4.6 and 4.7. You can see that it
features two standard BCB event handlers, as well as a WM_COMMAND
handler and the overridden WndProc virtual method.
Listing 4.6. The MENUDEF program shows how to retrieve
the ID of a BCB menu item.
#ifndef MainH
#define MainH
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <Menus.hpp>
class TForm1 : public TForm
{
__published:
TMemo *Memo1;
TButton *MenuID;
TMainMenu *MainMenu1;
TMenuItem *File1;
TMenuItem *Open1;
TMenuItem *CLose1;
TMenuItem *Exit1;
TMenuItem *Exit2;
TMenuItem *Cut1;
TMenuItem *Copy1;
TMenuItem *Paste1;
TListBox *ListBox1;
TButton *MiniWinSight;
void __fastcall MenuIDClick(
TObject *Sender);
void __fastcall MiniWinSightClick(
TObject *Sender);
private:
MESSAGE void WMCommand(TMessage &Message);
int TotalMenuItems;
int MenuItemArray[100];
protected:
virtual void __fastcall WndProc(Messages::TMessage &Message);
public:
virtual __fastcall TForm1(TComponent* Owner);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_COMMAND, TMessage, WMCommand);
END_MESSAGE_MAP(TForm);
};
extern TForm1 *Form1;
#endif
Listing 4.7. The MENUDEF program uses a TMemo, a
TButton, and a TMainMenu control.
///////////////////////////////////////
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
static bool ShowMessages = FALSE;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
int i;
TotalMenuItems = 0;
for (i = 0; i < ComponentCount; i++)
if (dynamic_cast<TMenuItem *>(Components[i]))
{
MenuItemArray[TotalMenuItems] =
dynamic_cast<TMenuItem*>(Components[i])->Command;
TotalMenuItems++;
}
}
void TForm1::WMCommand(TMessage &Message)
{
int i,j;
AnsiString S1(IntToStr(Message.WParam)), S3;
for (i = 0; i < TotalMenuItems; i++)
if (Message.WParam == MenuItemArray[i])
{
for (j = 0; j < ComponentCount; j++)
{
if (dynamic_cast<TMenuItem*>(Components[j]))
if (dynamic_cast<TMenuItem*>(Components[j])->Command ==
MenuItemArray[i])
S3 = dynamic_cast<TMenuItem*>(Components[j])->Caption;
}
S1 = "ID: " + S1 + "\rName: " + S3;
MessageBox(Handle, S1.c_str(), "Menu Item Info", MB_OK);
}
TForm::Dispatch(&Message);
}
void __fastcall TForm1::MenuIDClick(TObject *Sender)
{
int Command, i;
AnsiString Name;
Memo1->Lines->Clear();
for (i = 0; i < ComponentCount; i++)
{
if (dynamic_cast<TMenuItem*>(Components[i]))
{
Command = dynamic_cast<TMenuItem*>(Components[i])->Command;
Name = dynamic_cast<TMenuItem*>(Components[i])->Caption;
Memo1->Lines->Add(Name + " = " + IntToStr(Command));
}
}
}
void __fastcall TForm1::MiniWinSightClick(
TObject *Sender)
{
ShowMessages = True;
}
void ShowMsg(AnsiString &S)
{
if (Form1->ListBox1)
Form1->ListBox1->Items->Add(S);
if (Form1->ListBox1->Items->Count > 6)
Form1->ListBox1->TopIndex = Form1->ListBox1->Items->Count - 5;
}
void HandleMessages(TMessage &Msg)
{
AnsiString S;
switch(Msg.Msg)
{
case WM_PAINT:
S = "wm_Paint";
ShowMsg(S);
break;
case WM_MOUSEMOVE:
S = IntToStr(Msg.LParamLo) + " " + IntToStr(Msg.LParamHi);
S = "WM_MOUSEMOVE " + S;
ShowMsg(S);
break;
case WM_LBUTTONDOWN:
S = IntToStr(Msg.LParamLo) + " " + IntToStr(Msg.LParamHi);
S = "WM_LBUTTONDOWN" + S;
ShowMsg(S);
break;
case WM_RBUTTONDOWN:
S = IntToStr(Msg.LParamLo) + " " + IntToStr(Msg.LParamHi);
S = "WM_RBUTTONDOWN" + S;
ShowMsg(S);
break;
/* case WM_NCHITTEST: // Uncomment WM_NCHITEST to see a flurry of messages.
S = "WM_NCHITTEST";
ShowMsg(S);
break; */
}
}
void __fastcall TForm1::WndProc(Messages::TMessage &Message)
{
if (Message.Msg == WM_CLOSE)
ShowMessages = FALSE;
if (ShowMessages)
HandleMessages(Message);
TForm::WndProc(Message);
}
The MENUDEF program has two features:
- If you click the button at the bottom of the main form, the program's
memo control displays a list of all the items in the menu, along with
their IDs.
- If you click any of the menu items, a message box appears stating the name
of the menu item and its ID.
Here is the code that grabs the ID of all the menu items and displays the ID
along with the menu item's caption in a TMemo:
void __fastcall TForm1::MenuIDClick(TObject *Sender)
{
int Command, i;
AnsiString Name;
Memo1->Lines->Clear();
for (i = 0; i < ComponentCount; i++)
{
if (dynamic_cast<TMenuItem*>(Components[i]))
{
Command = dynamic_cast<TMenuItem*>(Components[i])->Command;
Name = dynamic_cast<TMenuItem*>(Components[i])->Caption;
Memo1->Lines->Add(Name + " = " + IntToStr(Command));
}
}
}
The code begins by clearing the current contents of the memo
control. It then iterates through all the components on the form and finds any
of them that are of type TMenuItem. The next step is to get the ID and
the caption of the menu items. To get the ID, you need only reference the
Command property of the TMenuItem component. The caption can be
retrieved the same way, and then you can add this information to the list box.
The use of a dynamic_cast in the previous code demonstrates BCB's
capability to work with Run Time Type Information (RTTI). RTTI enables you to
test the type of a particular variable and respond accordingly. For instance, in
this case the program simply asks whether a particular component is of type
TMenuItem; if this Boolean question returns True, the program
examines the component in more depth.
The remaining code in the program captures the IDs of the menu items in an
array and then responds to WM_COMMAND messages generated by clicks in
one of the program's menu items. As explained earlier, the code displays a
message box stating that the menus are not yet functional. In the caption of the
menu box, you can read the ID of the control that was clicked.
The code that captures the menu items in an array occurs in a Forms
constructor:
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
int i;
TotalMenuItems = 0;
for (i = 0; i < ComponentCount; i++)
if (dynamic_cast<TMenuItem *>(Components[i]))
{
MenuItemArray[TotalMenuItems] =
dynamic_cast<TMenuItem*>(Components[i])->Command;
TotalMenuItems++;
}
}
This code is almost identical to the MenuIDClick method, except that
the IDs of the TMenuItems are stored in an array rather than being
shown in a TMemo. The declaration for the array looks like this:
int MenuItemArray[100];
The declaration for the WM_COMMAND handler should be familiar to you
by this time:
MESSAGE void WMCommand(TMessage &Message);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_COMMAND, TMessage, WMCommand);
END_MESSAGE_MAP(TForm);
Here, the message directive tells the compiler that this is a dynamic method
and the offset in the dynamic method table is established by the WM_COMMAND
constant.
The WMCommand method compares the values sent in Message.wParam
to the values in the MenuItemArray. If it finds a match, it displays
the message box previously described.
void TForm1::WMCommand(TMessage &Message)
{
int i,j;
AnsiString S1(IntToStr(Message.WParam)), S3;
for (i = 0; i < TotalMenuItems; i++)
if (Message.WParam == MenuItemArray[i])
{
for (j = 0; j < ComponentCount; j++)
{
if (dynamic_cast<TMenuItem*>(Components[j]))
if (dynamic_cast<TMenuItem*>(Components[j])->Command ==
MenuItemArray[i])
S3 = dynamic_cast<TMenuItem*>(Components[j])->Caption;
}
S1 = "ID: " + S1 + "\rName: " + S3;
MessageBox(Handle, S1.c_str(), "Menu Item Info", MB_OK);
}
TForm::Dispatch(&Message);
}
This code calls the Windows API MessageBox function rather than
MessageDlg.
Here is the code that overrides the window function:
void __fastcall TForm1::WndProc(Messages::TMessage
&Message)
{
if (Message.Msg == WM_CLOSE)
ShowMessages = FALSE;
if (ShowMessages)
HandleMessages(Message);
TForm::WndProc(Message);
}
The TMessage struct looks like this:
struct TMessage
{
unsigned int Msg;
union
{
struct
{
unsigned short WParamLo;
unsigned short WParamHi;
unsigned short LParamLo;
unsigned short LParamHi;
unsigned short ResultLo;
unsigned short ResultHi;
};
struct
{
long WParam;
long LParam;
long Result;
};
};
};
This structure has all the information in it that you would get if you were
inside a standard windows function. For all intents and purposes, you are inside
a standard window function. (See TApplication.Create in the Pascal
source, as well as the TForm object, for more information.)
At this stage I could begin a 400- or 500-page analysis of window functions,
messages, and the structure of the Windows operating system. (That was pretty
much what I did in Teach Yourself Windows 95 Programming in 21 Days (Sams
Publishing), so there is a source for this kind of information if you want it.)
However, this book is not about that kind of material, so I will leave this
method hanging and let you explore it to the degree to which the subject matter
calls you. Remember, however, that this method can be dangerous to handle, as it
is active even when the controls on your form have not been created, and it is
still active after they have been destroyed.
One final point about working with the IDs of a control: If you build the
interface of your program and then don't change its menus or any other aspect of
its interface, the IDs that BCB associates with each control will (at least
theoretically) remain the same throughout the development of your application.
This might give some people the idea of using the MenuIDClick method,
shown earlier, as a temporary response to a click on one of your controls and
the storing of the output to a file. Thereafter, you would hope to know the ID
associated with each of your controls and could handle them in a WM_COMMAND
routine. This technique is theoretically possible, but I wouldn't recommend it
as part of the framework for any serious application. In short, if you have to
know the ID of a particular control, I would determine it in the FormCreate
method, thereby ensuring that the ID is correct for that build of your program.
Summary
In this chapter you learned how to handle events using the standard BCB
delegation model. You saw that when the need arises, you can circumvent this
system and handle events directly by employing message maps. You can also pass
messages back to the system by calling the inherited Dispatch method.
Events represent one of the most important subjects in BCB programming, and you
probably shouldn't move on until you have a good idea of what's going on in the
EVENTS2 program.
This chapter also explained that you can handle wParam and
lParam variables directly. Furthermore, BCB gives you a way to parse the
information passed
with events, so that you can place a custom record as the parameter to a
message handler. For instance, if Windows conceals a set of X and Y
coordinates inside the high or low words of wParam, BCB enables you to
define a custom record that automatically breaks up wParam into two
separate variables called X and Y. This is the same
functionality that is found in the message crackers provided in WINDOWSX.H.
The TWMMouse record discussed earlier is one of these records;
TMessage is another. If you want, you can create your own records that
parse the information associated with standard BCB events or with events that
you create yourself. You are not limited to custom BCB handlers such as
TMessage or TWMMouse.
|