Download demo project - 124 Kb
Download source files - 105 Kb
The problem
Several times now I've found the need for a pair of list
boxes that are linked. Items can appear in either of the list boxes, but not in both.
Items can be moved between list boxes, and have processes performed on them. Their order
within the list boxes can often be shuffled.
The first time I ended up with all of the code to do this
spread throughout the message handlers of the enclosing dialog box. Not very neat, but it
worked. Then I needed to do something very similar and found myself reinventing the wheel,
cutting and pasting from one dialog box to another. I stopped and pulled all of the code
out and placed it where it should be, in classes especially designed to solve my problem.
The result was a set of classes that can be plugged into
any dialog box with minimum fuss and do all of the work required for linked list boxes,
and much more besides.
Just a pair of list boxes?
As I thought about the problem I realised that the
include/exclude control was just a special case of a pair of the kind of extended list box
that I am always inventing. A list box with buttons closely associated with it. The
buttons allow the user to perform processes on the selected items in the list box, or move
items into, or out of the box. The buttons are often only enabled when items are selected
or when there are a certain number of items or more in the list box. Where only one button
is associated with the list box the action that the button performs can often also be
achieved by double clicking the item.
A better list box?
An extended list box that does just what we want can be
created as a subclass of
CListBox
. We can introduce the concept of a
"list box item processor", an abstract class which provides an interface for performing
processing on a list box item. A processor can then be associated with the list box so
that double click handling can be performed inside the list box rather than outside of
it. By associating a list of buttons with the list box, the buttons can have their
enabled/disabled state managed by it. When items are added or removed the state of all
associated buttons can be automatically adjusted. Where the buttons are required to
perform an action on a selected item, when pressed, we can associate a list box item
processor with them to allow them to process the item.
There are some peculiarities with the standard CListBox
class that means that it's hard to write code that can work with both a single selection
box and a multiple selection box without change. We can wrap up these differences so that
we have a consistent interface to either.
We can use message reflection so that all of the code for
our extended list box exists inside our CListBox
subclass. That way objects of our
subclass can handle their own messages rather than having the containing dialog box handle
them for us.
The end result is a list box that makes it easier to do
most of the "standard" things that you might want to do with a list box. All the
code is in one place, so it's easy to put it in a library and use it in lots of dialog
boxes. Associated buttons are managed automatically by the list box, and as a user you
just need to derive a class from the list box item processor to manipulate the items when
they are double clicked or when an associated button is pressed.
The classes involved
The following class diagram shows the classes involved in building our extended list box:
To take advantage of the extended functionality of
CJBListBox
simply use in place of a regular CListBox
. The extended
list box offers the following member functions: (see the source code for more information!)
SetDefaultProcessor()
- sets up a list processor to handle double clicks.
AssociateButton()
- associates a button with the list. If a button ID is
supplied then the list box will create and manage the button. If a list processor is
supplied it will be used to handle clicking on the button otherwise the box's default
processor will be used. If a reference to an existing CButton
is supplied,
rather than a button ID, then the list box will still manage the enabled state of the
button but the button can look after its own OnClicked()
processing.
AddItem()
- adds a string and optional data pointer to the list box and
updates the state of any associated buttons.
InsertItem()
- inserts an item at a specific index and updates the state
of any associated buttons.
RemoveItem()
- removes an item and any associated data pointer from the
list box and updates the state of any associated buttons.
GetSelectionCount()
returns the number of items selected whether the box
is single or multi selection.
GetSelectedItems()
- returns an array of indexes to selected items whether
the box is single or multi selection.
SelectItem()
- selects an item and updates any associated buttons.
CancelSelection()
- cancels either a particular selection, or any current
selection, and updates the state of any associated buttons.
MoveItemUp()
- moves the specified item up one slot in the list box.
MoveItemDown()
- moves the specified item down one slot in the list box.
ProcessSelectedItems()
- apply the list box's default processor, or a
supplied processor, to all selected items.
The only other class that a user of the list box must be aware of is
CJBListBox::ItemProcessor
. This is a very simple abstract base class.
class CJBListBox::ItemProcessor
{
public :
virtual ~ItemProcessor() { }
virtual void ProcessSelectedItem(
const int nIndex,
const CString &theString,
void *pData,
PostProcessAction_e &action) = 0;
};
ProcessSelectedItem()
gets called to process the list box
item and gets passed the item's index, string and any data stored with it. The enum
returned can be set to indicate what should be done with the item after the function
returns (deselection, deletion, etc).
If multiple items are selected then each will be passed in
turn to the list processor. This is possibly a weakness of the design, but I have yet to
come across a situation where I needed to know about all of the items in a multi-selection
before I could process any of them.
Known problems with item manipulation
At present the code doesn't handle a multiple selection
move up or down which retains the items as selected after the move. This is because I
can't find a way to build an extended selection programatically. Also I don't expect
multiple selections where some items are moved and some deleted to work. I haven't tried
too hard to fix these as I don't require the functionality - if you do require it, and you
do fix up the code to handle it, please let me know!
Other related classes
Due to the member functions supplied by our extended list
box it's easy use list item processors to rearrange the order of items within the list
box, or simply remove items. These kinds of item processors are just "simple item
processors" since all they do is return an action to be performed. Since they're
handy to have I provided a template simple item processor class. To use just instantiate
with the action you wish to perform, e.g.
CJBListBox::TSimpleItemProcessor<CJBListBox::MoveDown> downProcessor;
CJBListBox::TSimpleItemProcessor<CJBListBox::Delete> deleteProcessor;
To nest, or not to nest...
All of the classes involved are contained in a namespace.
Initially I thought that was good enough, but I realized that many of the classes were
merely implementation details of other classes and weren't for external use. I'd already
placed the declaration for the list node class inside the list itself. I followed this
practice to its logical conclusion by placing the button inside the list, and the list and
item processor inside the
CJBListBox
class. I've yet to decide if this has added
complexity, or whether it was necessary, and I had to jump through a few hoops to keep
class wizard happy.
Nesting the classes has added a tighter level of coupling
to the classes. In one way this is good because the classes that are coupled are
implementation details of the classes they are coupled to. However, I dislike the way that
more source files are dependant on each other.
Building the include/exclude control
Once we have built this extended list box solving our
original problem is easy. We just take two of our extended list boxes, write list item
processors that removes an item from one box and inserts it into the other, wire up a few
buttons, and we're done.
The classes involved
Once we have the extended list box, the rest is easy...
CJBListBoxPair
is a very simple class. It has two public
CJBListBox
data members and a nested, private, list swap item processor. The
control can be simply added to a dialog box and hooked up to the controls by calling one
function: Create()
which takes the control IDs of the two list boxes and the
two buttons which move items from one box to the other. The control can be extended by
associating more buttons with the list boxes if required.
The decision to make the two list boxes public data
members was based on the fact that if we didn't make them publicly accessible then
forwarding functions for each of the useful member functions would need to be written.
These forwarding functions would also need to be able to determine which of the two boxes
they should be applied to. This seemed unnecessary.
The list swap item processor was relatively easy to
create. The processor needs to be given a list box to add items to. When its
ProcessSelectedItem()
function is called it simply adds the item to its list box and
returns "Delete" to delete the item from the box it was originally in.
See the article on Len's
homepage for the latest updates.