March 1998

Synchronizing two list boxes

by Matt Telles

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.

What's really going on in there?

When you click on the vertical scrollbar of a list box, a number of things happen. First, Windows catches the fact that you clicked the mouse in a window. Next, that window is queried to find out where the click happened. When the message is routed to the scrollbar, the scrollbar then informs the parent window (in this case, the list box) that the user has asked to scroll the list. It sends this information via a Windows message. Windows messages form the heart of the entire event-handling system that the VCL uses to "talk" to the outside world. Events are really just Windows message handlers that have been customized by the nice folks at Borland (or whoever wrote the component) so you can interface with them without knowing anything more about them. When you want to work with messages that aren't directly exposed, it takes a little more work.

Creating the synchronized list component

The first step in building a list box you can synchronize with another list box is to create a component that encapsulates the problem. To do this, you need to create a component based on the TCustomListBox.

Tip: Deriving from existing components

When you derive components from existing C++Builder components to extend functionality, always look for a TCustomxxx version of the component (where xxx is the name of the component you want to extend). All the basic functionality of the component will be exposed at this level without the overrides provided by levels farther down the food chain.

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.

Adding the message handler

Once you've generated the component skeleton, the next step is to create the specific code to extend this component. In this case, that means trapping for a specific Windows message and doing something when you get it. You're looking for the Windows message WM_VSCROLL, which is sent when the user clicks a control's scrollbar. The message's contents include the type of click (moving up or down a row, paging up or down, or dragging the scrollbar up or down) and the name of the control that should receive that data. All this information is contained within a Windows MSG structure. In the Borland Delphi/C++Builder world, the information is further contained within the TMessage class.

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.

Implementing the scroll handler method

Let's look at the implementation of the scroll handler method first, then fill in the details and missing pieces. Add the new method to your class implementation file (cpp) and enter the following code into the method:

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.