Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Horizontally Scrollable Listboxes


SP93: Horizontally Scrollable Listboxes

Horizontally Scrollable Listboxes

Displaying strings of varying and unknown length

Ted Faison

Ted is a writer and developer, specializing in Windows and C++. He has authored two books on C++. He is president of Faison Computing, a firm which develops C++ class libraries for DOS and Windows. He can be contacted on CompuServe at 76350,1013.


Windows listboxes are typically used to show lists of files, fonts, or other variable-length lists of textual information. To add a listbox to a dialog box, you generally edit a resource file using programs such as Microsoft's Dialog Editor or Borland's Resource Workshop. Windows transparently handles most of the listbox details. For example, if you add strings to a listbox, Windows will automatically put a scroll bar on the control when the listbox contains more strings than can be displayed in the client area. Windows also handles scroll bar events without any need for application code.

Displaying a list of files in a listbox is generally easy because filenames have a predefined maximum number of characters. But, if you plan to use a listbox to display strings of varying and unknown length, such as the names of people or the titles of your CD collection, creating a listbox wide enough to accommodate the widest string you expect isn't always practical. An alternative is to make the listbox wide enough to handle the average string, using a horizontal scroll bar to scroll the client area when strings are too long. One solution is to create a listbox using a tool such as Resource Workshop, and to set the horizontal scroll bar property (or enable the WS_HSCROLL style bit). Unfortunately, when you display the listbox in your application, you'll find the horizontal scroll bar doesn't appear. The reason is that Windows doesn't handle all aspects of horizontal scroll bars in listboxes. It's up to the application to supply code to make horizontal scroll bars work correctly.

In this article, I'll discuss using Borland C++ to show how to create a listbox class, which will manage all of the details necessary to scroll horizontally within a listbox. In the process, I'll present a sorted container class, derived from Borland's template-based BIDS library, that keeps track of text extents. Finally, I'll present a sample application to take advantage of the horizontal listbox class to copy long strings from one listbox control to another.

Text Extents

When you use the WS_VSCROLL style with a listbox, Windows keeps track of the number of items added to the control. Knowing the size of the listbox's font, the number of items in the listbox, and the height of the listbox's client area, Windows can determine when there are more items than will fit on the control. When this condition is met, Windows adds the vertical scroll bar to the listbox.

It's more complicated with horizontal scroll bars because the width of a string is dependent on both the font and the length of the string. The width of a string measured in pixels is called the "text extent." Windows doesn't automatically keep track of the extents of strings inserted into listboxes. That's where your application code comes into play. When you add a string to a listbox, you must tell Windows the extent of the string. When you indicate an extent that exceeds the width of the listbox's client area, Windows draws the horizontal scroll bar. You tell Windows the extent of a string using LB_SET-HORIZONTALEXTENT. The wParam parameter indicates the extent of the string.

When you add more than one string to a listbox, Windows only needs to know the extent of the widest string. This means that your application has to keep track of the extents of all the strings in a listbox. The management of text extents takes a significant amount of effort using ordinary C code. The job is considerably simpler using C++ containers and a Windows application framework such as Borland's Object Windows Library (OWL).

Containers for Text Extents

In C++, containers can hold any kind of object including ints, chars, pointers, and class objects. A simple container is not quite sufficient to handle our text extents because we need to know the extent of the widest string in the listbox. This requires a sorted container that stores the text extents in ascending order. Each time a string is added to the listbox, its extent is computed and added to the container. With a sorted container, the greatest extent will always be the last item in the container.

The implementation of a sorted container restricts the type of objects that can be put into the container. When a new item is added, the container needs to compare the new item with those already in the container in order to locate the correct insertion point. The comparison is achieved by calling the operator< and operator== member functions for the item inserted. Because member functions are invoked, only class objects can be used in sorted containers.

Borland provides two container class libraries: the object container library, which is easier to learn and use, but less efficient in terms of performance; and a template-based BIDS (Borland International Data Structure) container class library. In this article, I use BIDS. With BIDS containers, objects inserted into a sorted container must have the member functions shown in Figure 1. A small class called "Extent'' to handle the text extents is shown in Listing One, page 50. Having a class for the text extents, we can create a template-based container to manage the extents. Using BIDS containers, the name of the class we need is BI_SArrayAsVector. BI refers to Borland International; S indicates a sorted array. To create a container called MyContainer to hold Extent objects, use the notation: BI_SArrayAsVector<Extent> MyContainer;. Anytime you define a template-based variable, the C++ compiler creates a completely new class based on the template variable.

Before you can add an extent to the container, you need to determine the extent for the string. TextExtent() (see Listing Two, page 50) computes the extent. A WM_GETFONT message is sent to the listbox to retrieve the handle of the font used in the control. If the system font is being used (which is often the case), a NULL handle is returned. If a non-system font is being used, it must be placed in the listbox's device context before calling the Windows API GetTextExtent. The code in Figure 2 adds an extent to the container. The textExtents variable is the extent container, and AString is a pointer to a null-

terminated array of characters. The extent object inserted into the container is created with the global new operator. You have to be a little careful with objects that are put into containers, because by default containers own the items they contain. This means that when the container goes out of scope, it will try to delete all the items that are still in it. If you've put a stack-based or global object in the container, your application will likely crash, since the delete operator may be called only for objects created by the new operator. To place stack-based or global objects into a container, you must tell the container that it doesn't "own" the elements in it. You do so by calling the container's member function ownsElements(), passing the value 0. Once ownership is disabled, the container will never try to delete any of the items in it.

Custom Controls with OWL

One way to handle horizontal scroll bars is to use a standard listbox and make the parent window do all the work of managing the text extent container, sending WM_SETHORIZONTALEXTENT messages to the listbox, and so on. A better, more object-oriented approach is to create a new control that does all the housekeeping by itself, without disturbing the parent. With OWL, building a custom control of this type is not difficult. OWL already defines the class TListBox to act as an object-oriented stand-in for regular Windows listboxes. All you have to do is derive a new class from TListBox and add the necessary support for horizontal scroll bars. THorizontalListBox (see Listing One) encapsulates all the details described.

Because class Extent is designed to be used exclusively inside THorizontal-

ListBox, it is declared as a nested class. Therefore, Extent is only in scope inside THorizontalListBox. The template-based container is typedefed to improve code readability. Using the type Extents is a lot easier than having to use BI_

SArrayAsVector<Extent>. The textExtents data member is the actual container for the text extents. The base class functions AddString, InsertString, Delete-

String, and ClearList are overridden, because every time a string is added or removed from the listbox, the extents container needs to be updated and the listbox's horizontal extent kept up to date.

InsertExtent computes a string's extent, adds it to the extent container, and updates the listbox's horizontal extent. The base class function AddString is then called to actually add the string to the listbox. The DeleteString function (see Listing Two) is similar to AddString, except that it needs to locate the extent of the string being removed from the listbox, delete the extent object, then update the listbox's horizontal extent. A short loop searches the extent container for the extent of the string being removed from the listbox. The textExtents.detach() statement removes the extent object from the container and deletes the object itself. UpdateHorizontalExtent() sets the extent of the listbox using the greatest extent still stored in the extent container. The base class function TListBox::DeleteString does the actual work of removing a string from the listbox.

The Transfer Buffer

The only tricky feature of class THorizontalListBox is the way it handles data that is inserted into the listbox directly from a transfer buffer. Using OWL, child controls can be set up to operate with a special buffer called a "transfer buffer.'' When the child control is created, it reads its initial data from the transfer buffer. When the child control is destroyed, its data can be saved into the transfer buffer. Using a transfer buffer drastically simplifies the task of reading and writing data to and from child controls.

When strings are put into a THorizontalListBox object from a transfer buffer, OWL calls the function THorizontalListBox::Transfer(), passing TF_SETDATA as the second parameter (see Listing Two). THorizontalListBox needs to invoke the base class to successfully copy the string data from the transfer buffer into the control. Then the extent container must be updated for each string in the control. The first parameter passed to Transfer() is a pointer to a pointer to a TListBoxData object. This object contains an array data member called Strings, which holds the strings to be copied into the listbox. After calling the base class to copy the strings, Transfer() uses the forEach iterator function to iterate over the text extents stored in the container. For each extent found, the function is called to update the container object.

A Short Example

I wrote a short OWL application, HORSCROL, to demonstrate the use of the THorizontalListBox class. The source code is provided electronically; see "Availability," page 3. HORSCROL initially displays a dialog box (see Figure 3). Using the >> and << buttons, you can move selected entries from one listbox to the other and see the way the horizontal scroll bar is affected. Windows displays the scroll bar only if a listbox contains strings that are wider than the listbox.

To get Windows to display a horizontal scroll bar, a listbox must be created with both the WS_VSCROLL and WS_HSCROLL styles. If you omit the WS_VSCROLL style, Windows adds it automatically. The physical act of displaying a horizontal scroll bar is independent from the display of a vertical scroll bar. For example, adding a long string to the right listbox causes a horizontal scroll bar, but no vertical one to appear.

Conclusion

Managing horizontal scroll bars in listboxes is not something application programs should be required to do. Windows should bear the responsibility for providing full support, just as it does with vertical scroll bars. Complete support is likely to appear in the next release of Windows, but in the meantime developers are stuck rolling their own scroll bars. Microsoft provides a technical bulletin, available both on Compu-

Serve in the Developer's Network Forum, and on the Developer's Network CD, that describes how to implement horizontal listboxes in C, and includes DLL code to support horizontal listboxes for C applications.

References

Brockschmidt, Kraig and Kyle Marsch, "Considerations for Horizontal Scroll Bars in listboxes," Microsoft Developer's Network CD, March 1992.


Figure 1: Required member functions for objects inserted into a sorted container.

Default constructor
Copy constructor
Operator ==
Operator <
isSortable()


<B><a name="03de_000c">Figure 2:</B> Adding an extent to the Extent container.<a name="03de_000c">

int length = TextExtent(AString);
Extent extent = *new Extent(length);
textExtents.add(extent);

Figure 3: Sample use of horizontally scrollable listboxes.


[LISTING ONE]


#ifndef __HLBOX_HPP
#define __HLBOX_HPP

#include <arrays.h>
#include <owl.h>
#include <listbox.h>

class THorizontalListBox: public TListBox {

  class Extent {
    int value;
  public:
    Extent(int i) {value = i;}
    Extent() {value = 0;}
    Extent(Extent& i) {value = (int) i;}
    int operator==(Extent& i) const {return value == (int) i;}
    int operator<(Extent& i) const {return value < (int) i;}
    operator int() {return value;}
    virtual int isSortable() {return 1;}
  };
  typedef BI_SArrayAsVector<Extent> Extents;
  Extents textExtents;
  int TextExtent(LPSTR);
  void UpdateHorizontalExtent();
public:
  THorizontalListBox(PTWindowsObject, int, PTModule = NULL);

  int AddString(LPSTR);
  int InsertString(LPSTR, int);
  int DeleteString(int);
  void ClearList();
  void InsertExtent(LPSTR);
  virtual WORD Transfer(void*, WORD);
};
#endif

[LISTING TWO]

#include "hlbox.hpp"

THorizontalListBox::THorizontalListBox(TWindowsObject* AParent,
  int id, TModule* module):TListBox(AParent, id, module),textExtents(20, 0, 20)
{
}
static void AddTextExtent(Object& AString, void* P)
{
  THorizontalListBox* listBox = (THorizontalListBox*) P;
  LPSTR string = (char*)(const char*)(RString)AString;
  if (AString != NOOBJECT)
    listBox->InsertExtent(string);
}
WORD THorizontalListBox::Transfer(void* DataPtr, WORD TransferFlag)
{
  WORD value = TListBox::Transfer(DataPtr, TransferFlag);
  if (TransferFlag == TF_SETDATA) {
    // for each string in the transfer buffer,
    // add its extent to the extent container
    TListBoxData* ListBoxData = *(PTListBoxData*) DataPtr;
    ListBoxData->Strings->forEach(AddTextExtent, this);
  }
  return value;
}
int THorizontalListBox::AddString(LPSTR AString)
{
  InsertExtent(AString);
  return TListBox::AddString(AString);
}
int THorizontalListBox::InsertString(LPSTR AString, int Index)
{
  InsertExtent(AString);
  return TListBox::InsertString(AString, Index);
}
void THorizontalListBox::InsertExtent(LPSTR AString)
{
  // store the extent of each string in sorted order in a container
  int length = TextExtent(AString);
  Extent extent = *new Extent(length);
  textExtents.add(extent);
  // update the ListBox horizontal extent
  UpdateHorizontalExtent();
}
int THorizontalListBox::DeleteString(int Index)
{
  // find the text extent of the string to be deleted
  char string [256];
  GetString(string, Index);
  // remove the extent from the container
  Extent extent = Extent(TextExtent(string) );
  for (int i = 0; i < textExtents.getItemsInContainer(); i++) {
    if (extent == textExtents [i]) {
      textExtents.detach(extent, TShouldDelete::Delete);
      break;
    }
  }
  // update the ListBox horizontal extent
  UpdateHorizontalExtent();
  return TListBox::DeleteString(Index);
}
void THorizontalListBox::ClearList()
{
  // delete all the text extents in the container
  textExtents.flush();
  // update the ListBox horizontal extent
  UpdateHorizontalExtent();
  // Call DeleteString, to force Windows 3.0 to remove
  // the horizontal scrollbar. Windows 3.1 doesn't need this call
  DeleteString(0);
  // clear out the remaining strings in the ListBox
  TListBox::ClearList();
}
// find the extent of a ListBox string
int THorizontalListBox::TextExtent(LPSTR AString)
{
  int extent;
  // select the ListBox into the device context
  HDC hdc = GetDC(HWindow);
  HFONT hfont = (HFONT) SendMessage(HWindow, WM_GETFONT, 0, 0);
  if (hfont) {
    // non-system font being used: select it into the
    // ListBox's device context before calling GetTextExtent
    HGDIOBJ oldObject = SelectObject(hdc, hfont);
    // find the text extent of the string
    extent = GetTextExtent(hdc, AString, _fstrlen(AString) );
    // release resources used
    SelectObject(hdc, oldObject);
    ReleaseDC(HWindow, hdc);
  }
  else {
    // system font in use: no font selection necessary,
    // because GetTextExtent will use the system font by default
    extent = GetTextExtent(hdc, AString, _fstrlen(AString) );
    ReleaseDC(HWindow, hdc);
  }
  return extent;
}
void THorizontalListBox::UpdateHorizontalExtent()
{
  int greatestExtent;

  // find the extent of the longest string in the
  // ListBox, and set the horizontal extent accordingly
  int lastElement = textExtents.getItemsInContainer() - 1;
  if (lastElement < 0)
    // no more strings in the ListBox
    greatestExtent = 0;
  else
    greatestExtent = textExtents [lastElement];
  // add a small amount of space, so that when ListBox is completely scrolled
  // to the right, the last character is completely visible
  HDC hdc = GetDC(HWindow);
  greatestExtent += GetTextExtent(hdc, "X", 1);
  ReleaseDC(HWindow, hdc);
  // if the longest string fits completely in the ListBox, then scroll the box
  // completely to the left, so Windows will hide the scrollbar
  RECT rect;
  GetClientRect(HWindow, (LPRECT) &rect);
  int listWidth = rect.right - rect.left;
  if (listWidth >= greatestExtent)
    SendMessage(HWindow, WM_HSCROLL, SB_TOP, 0);
  // set the extent
  SendMessage(HWindow, LB_SETHORIZONTALEXTENT, greatestExtent, 0);
}
End Listings



Copyright © 1993, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.