The following question came up recently on one of the borland.public newsgroups: How do you synchronize two list boxes so that when one of them scrolls, the other will scroll as well? The responses ranged from simply trapping for list-box changes to overriding the owner-draw aspects of the list itself to watch for scrolling. I found it interesting that so many people would go to such lengths to solve what is really a pretty simple problem. A good solution was eventually posted, but by then I'd written my own. The essence of this synchronization question lies in understanding the Windows messaging system. You really can't program in Windows without knowing at least a little something about what's going on "under the hood." (For a detailed discussion on messages, see "Incorporate Custom Message-Handling in Your Applications" in the Premiere issue of C++Builder Developer's Journal.) However, with the advent of programming environments like C++Builder, Delphi, and even Visual Basic, programmers have been shielded from the horrors of "real" Windows programming. Since this list box problem would allow me to scratch the surface of the Windows programming model and expose some of the stuff underneath without writing an entire tome (something I've also done), I thought it would make a good article.
If you've never created a component from scratch, don't fret, it's easy! (For an overview of the process, see the three-part "Building Components" series in the September, October, and November 1997 issues of C++Builder Developer's Journal.)
First, select Component | New from the main IDE menu. You'll see a dialog box that asks for the name of your new component; a combo box lets you select the base class to extend. Enter TSyncListBox as the new component's name, then select TCustomListBox for the base class and click OK. The wizard will do the rest, generating the basic files needed for creating and registering the component in the C++Builder system.
To handle a new component message, you need to add a message map to the component. Borland provides macros to make this task easy. In the component's header file, add the following lines in the protected section of the component definition:
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_VSCROLL, TMessage,
HandleVScroll)
END_MESSAGE_MAP(TCustomListBox)
This
block of code defines a new message entry for the class to handle the
WM_VSCROLL message. The second item in the MESSAGE_HANDLER macro call
(TMessage) indicates the type of message structure this method expects to
receive. For some messages, you'll probably create your own custom structures
based on TMessage, but in this case you'll use the plain TMessage structure.
The macro's final argument is the name of the method you want called when the
component receives the message.
Once you've told the compiler which method to call when the message is
received, you must define that method. To do so, add the following line to the
private section of the component's header file:
virtual void __fastcall HandleVScroll(
TMessage& msg );
You
might wonder why the method is defined as private if the system is going to
call it. The reason is simple: The method isn't called by the system at all! In
fact, the internals of the object call this method.
Here's how it works. When you use the BEGIN_MESSAGE_MAP macro in your header
file, you're defining an inline method called Dispatch. C++Builder calls the
Dispatch method for each message received by a component. If you don't define
the Dispatch method, it's automatically handled for you by the TObject class,
from which all components (and everything else) are derived.
If you look at the definition of the BEGIN_MESSAGE_MAP, MESSAGE_HANDLER, and END_MESSAGE_MAP methods, you'll see that the code generated for the items you just added to the component header file look like this:
virtual void __fastcall Dispatch(
void *Message_)
{
switch (((PMessage)Message)->Msg)
case WM_VSCROLL:
HandleVScroll(*((TMessage *)Message));
break;
default:
TCustomListBox::Dispatch(Message);
break;
}
}
As
you can see, when a WM_VSCROLL message is received, the switch statement
determines which message it is. If it is, in fact, a WM_VSCROLL message, it's
routed to the HandleVScroll method. Since Dispatch is a method within the
class, it can safely call the private method for you. All that's left for you
to do is to write the actual implementation of the class.
void __fastcall TSyncListBox::HandleVScroll(
TMessage& msg )
{
// Let the default occur
TCustomListBox::Dispatch( &msg );
// Now, sync the two if necessary
if ( pSyncBox )
pSyncBox->TopIndex = TopIndex;
}
The
actual processing is easy. First, you let the underlying Windows list box deal
with the event as it normally would--which might cause the list box to scroll
in either direction. If it does, you simply tell the list box that's
synchronized with this one to move to the same position. You needn't worry
about which way the list box scrolled or even if it scrolled! The
TopIndex of the TListBox class allows you to control the index of the item that
appears at the top of the list box display.
All that's missing is the code that tells the list box to synchronize itself
with another list. You need to add to the class a member variable representing
the other list box and a method allowing the programmer to set this member
variable. This code goes in the component's header file; Listing A
contains the complete header file for the class.
Listing A: TSyncListBox.h
//---------------------------------------------
#ifndef TSyncListBoxH
#define TSyncListBoxH
//---------------------------------------------
#include <vcl\SysUtils.hpp>
#include <vcl\Controls.hpp>
#include <vcl\Classes.hpp>
#include <vcl\Forms.hpp>
#include <vcl\StdCtrls.hpp>
//---------------------------------------------
class TSyncListBox : public TCustomListBox
{
private:
virtual void __fastcall HandleVScroll(
TMessage& msg );
TCustomListBox *pSyncBox;
protected:
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_VSCROLL, TMessage,
HandleVScroll)
END_MESSAGE_MAP(TCustomListBox)
public:
__fastcall TSyncListBox(TComponent* Owner);
void SetSyncListBox(
TCustomListBox *pListBox )
{
pSyncBox = pListBox;
}
__published:
};
//---------------------------------------------
#endif
Finally, you must initialize the list box pointer to NULL; otherwise, you'd
have no way of knowing whether there's anything attached to this list with
which to synchronize. Besides, when you have a pointer as a member variable,
it's essential to initialize it before you use it. Since the class constructor
is intended to handle all the initialization code, that's where the change goes:
__fastcall
TSyncListBox::TSyncListBox(TComponent* Owner)
: TCustomListBox(Owner)
{
pSyncBox = NULL;
}
That's
all there is to it! With a couple of lines of code, you've created a brand new
component for use in your own applications.