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
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.
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 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.
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:
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 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.
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.
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
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.
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.
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:
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.
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.
©Copyright,
Macmillan Computer Publishing. All rights reserved.