How to Create Listboxes With Multi-Line Items

Introduction

In some applications it is difficult to show an item on one line of a listbox. If items stretch over more than one line, users will only be able to select one line unless multiple selection is invoked. Multiple selection allows the user to pick one line from many multiple line items (not what is wanted in this case). This document shows how to create a listbox that allows users to select a group of related lines (as shown below) on a single click of the left mouse button without allowing more than one group to be selected. Groups may contain an arbitrary number of spaces above the actual select and an arbitrary number below.

Notice that in this setup a blank line was inserted between each (multiple line) item. This version of the program selects only actual items. Notice that in the sample code one line was inserted above each entry and two below.

The Actual Listbox

When creating the listbox in the resource workshop make sure to use the following settings in the properties listbox.

Note in particular I have deselected sort (to maintain the order relationship with an array of items), and selected Multiple Select and Extend Select.

The Multi-Line Listbox Class

As in the case of the Right-Button Listbox, we create a new class that is a descendent of TListBox. This class will have it's WM_LBUTTONDOWN message handler overridden. The handler for this message just calls a modified version of the setSelectIndex function from the Right-Button Listbox. If I were really on the ball, I would have made setSelectIndex virtual, and made class MultiSelect a descendent of RightButtonListBox. I recreated the RightButtonListBox class to make setSelectIndex virtual. With this change both left and right button clicks have multiple selects.

The Code

Because we have a multiple select listbox, we must make sure that when a new item is selected, that any existing selection is deselected. Thus the first part of the function asks if there is anything selected. If there is, it is deselected. Next, as in the setSelectIndex function, it finds the index selected. This is the actual line number of the item selected. Divide this index by the size of a block containing one item. This gives the number of the item selected. Each item starts on a multiple of the blocksize, so I multiply the item number by the blocksize to find the top of the block. Next I select all lines in the block. Note that I have commented out the Borland code calling on the ListBox left button handler, and that the number of the item selected is the index div the block size.

void MultiSelect::setSelectIndex(TPoint &point)
{
    int selBlock = _blockSize - _spaceAbove - _spaceBelow;
    int *sels = new int[selBlock];
    if (GetSelCount()>0)
    {
    	GetSelIndexes(sels,selBlock);
    	SetSelIndexes(sels,selBlock,false);
    }
    int index = GetTopIndex();//the index at the top of the window
    int pos = 0; // y position of each index item
     while (pos < point.y) // current index is not the selected one
        pos += GetItemHeight(index++); //get next list position
    //Get top item in the group
    // Notice that top is the position of the item block selected,
    // but if each block represents one item in an array, the
    // actual item subscript is top / BlockSize+_spaceAbove.
    int top = _blockSize*(index / _blockSize)+_spaceAbove;
    for (int i = 0;idelete []sels;    
}

void MultiSelect::EvLButtonDown (uint /*took out unused parameter*/, TPoint& point)
{
     //******* Make sure to comment out next line.
     //TListBox::EvLButtonDown(modKeys, point);
     //***** Make sure to comment out the line above
     // INSERT>> Your code here.
    setSelectIndex(point);
}

I defined _blockSize, _spaceAbove and _spaceBelow as members of the class MultiSelect. They are initialized from parameters of the constructor. To get the right-button handler I added EvRButtonDown for MultiSelect. I just replaced the call to TListBox::EvRButtonDown by RightButtonListBox::EvRButtonDown(modKeys, point); Recall that the constructors for MultiSelect all call the TListBox constructor. Each of these is replace by the appropriate call to the RightButtonListBox constructor.

Using the Multi-Line Listbox

As with the right-button listbox, TListBox is replaced by MultiSelect in the lines where the listbox of the dialog is created. Usually this is in the constructor for the dialog or its SetupWindow function. In the zipped example the replacement takes place in the constructor. In the example each entry is 3 lines long. It is preceded by a space and followed by two. The total block size is 5 (3 + 1 + 2), the space above is 1 and the space below is 2, thus the constructor call is

 new MultiSelect(5,1,2,this, IDC_TEST_BOX);

TestListBox::TestListBox (TestListBoxXfer *tb,TWindow* parent, TResId resId, TModule* module):
    TDialog(parent, resId, module)
{
 //{{TestListBoxXFER_USE}}
    _list = new MultiSelect(5,1,2,this, IDC_TEST_BOX);

    SetTransferBuffer(tb);
 //{{TestListBoxXFER_USE_END}}

     // INSERT>> Your constructor code here.

}

When you use a transfer buffer to return a value, the results will be stored in a TIntArray. If you are working directly with a MultiSelect listbox, I have overridden the GetSelIndex and SetSelIndex functions for a standard list box to set and get the index of a block.