[ Next ] [ Previous ] Chapter 15
[ Contents ]  [Chapter 14: Menus ] [ Chapter 16: Buttons ]

List Boxes

A list box (see Figure 15.1) is a control that provides the user with a list of choices. Single or multiple items can be selected; the default is single. A list box can scroll horizontally, vertically, or both. List boxes, by default, contain only text entries, although they are not limited to only text.
A listbox control
The items in a list box should be presented in some order meaningful to the user. A list box should be large enough to have six or eight choices visible at all times and wide enough to display an item of average width without horizontal scrolling. If multiple selection is supported, informative text should be provided to indicate the current number of selected Items.
Figure 15.1 A list box control.

List Box Styles

The styles presented in Table 15.1 can be used when creating a list box.
Table 15.1 List  Box Styles
Style  Description
LS_MULTIPLESEL Supports selection of multiple items
LS_OWNERDRAW Generates a WM_DRAWITEM whenever certain parts are to be drawn.
LS_NOADJUSTPOS Will not size the list box
LS_HORZSCROLL Will have a horizontal scroll bar along the bottom and will support horizontal scrolling.
LS_EXTENDEDSEL Lets the user select more than one item using a point-end-point selection technique.

Extended Selection

List boxes also support a selection technique known as extended selection. Extended selection supports a "swiping" technique to select the list box items. Table 15.2 shows the keystrokes and mouse actions defined in a extended-selection list box.
 
Table 15.2Extended Selection List Box Keystrokes
Movement  Action
Click mouse button on object Selects object; all others are deselected.
Drag mouse from start point of selection to end point of selection Selects all objects in area; all other objects are deselected.
Press SHIFT key while cursor is at start point and use arrow upandarrow downkeys to move to end point  Selects all objects in area; all other objects are deselected.
Click mouse button on object while pressing Ctrl key Selects object; all other selected objects are left selected.
Press Ctrl+spacebar, or spacebar while cursor is positioned at object Selects object; all other selected objects are left selected.
Press Ctrl key while dragging mouse from start point of selection to end point of selection Selects all objects in area; all other objects are deselected.

The following LIST1 example program shows a very introductory list box program. This list box has the LS_MULTIPLESEL style and communicates with the client area to have the selections displayed in the window.

LIST1.C
LIST1.RC
LIST1.H
LIST1.MAK
LIST1.DEF

In the LIST1 sample program, the dialog box will post a message, UM_LISTBOXSEL, to the client area when the OK button is pressed. When the client area receives this message, it queries the list box to determine which items have been selected. These items are stored in the user-defined window word area for the client window. Also a flag, fSelectedItems, is set to indicate items have been selected.
When the WM_PAINT message is received, the client area is cleared. If the flag fSelectedItems is set, the items in the window word are written to the client area.
 

Initializing the Client Window

The structure LISTBOXINFO is used to hold the list box information
    typedef    struct
    {    USHORT   ausListBoxSel[NUM_ENTRIES];
    } LISTBOXINFO,*PLISTBOXINFO;

The array   ausListBoxSel[] is used to hold the items that have been selected.
The WM_CREATE message processing is were the memory is allocated for the structure LISTBOXINFO. WinSetWindowPtr is used to assign the pointer to the structure  pliInfo to the window word.

Initializing the List Box

            hwndListBox = WinWindowFromID(hwndDlg,
                                          IDL_LISTBOX);
            for (i = 0; i < NUM_ENTRIES; i++)
               WinInsertLboxItem(hwndListBox,
                                 LIT_END,
                                 pszListBoxEntry[i]);

The WM_INITDLG message processing initializes the list box. The first step is to obtain the window handle of the list box using WinWindowFromID. The dialog box is the parent of all the controls in it. The macro WinInsertLboxItem is a shortened version of the function WinSendDlgItemMsg, designed specially to insert items into list box. The first parameter is the list box window handle, hwndListBox. The second parameter indicates the position in the list box to insert the item. Acceptable entries are either an integer value indicating the placement of the item (0 indicates the topmost item) or the constant LIT_END. Also, the list box control is smart enough to sort the items alphabetically. The constants LIT_SORTASCENDING and LIT_SORTDESENDING can be used to automate this process. Alphabetization takes some time, though; sorting the list box items before inserting them in the list box may increase performance. The last parameter is the text string to enter into the list box.  The header file LISTBOX.H contains the definition for pszListBoxEntry.

                        WinSendDlgItemMsg(hwndDlg,
                              IDL_LISTBOX,
                              LM_SELECTITEM,
                              MPFROMSHORT(0),
                              MPFROMSHORT(TRUE));

One other nit about the list box: The first item must be selected manually. The message LM_SELECTITEM will do this for us. The first parameter is the index of the list box item to be selected. The second parameter indicates whether the item is selected (TRUE) or deselected (FALSE). Notice that this time we use the function WinSendDlgItemMsg; this is another way to send messages to items in a dialog box.

The WM_COMMAND Message Dialog Processing

                  hwndClient = WinQueryWindow(hwndDlg,
                                              QW_OWNER);
                  if (!hwndClient){
                    DisplayError("WinQueryWindow Failure:1");
                    break;
                  }
                  WinPostMsg(hwndClient,
                             UM_LISTBOXSEL,
                             MPVOID,
                             MPVOID);

                  /*********************************************/
                  /* if hit OK, don't dismiss dialog           */
                  /*********************************************/

                  return (MRFROMSHORT(TRUE));


When the user presses either the Ok or the CANCEL button,  the system sends a WM_COMMAND message to the dialog box. mpParm1 contains the ID of the pushbutton, either DID_OK or DID_CANCEL. If the user presses DID_OK, the system sends a user-defined message, UM_LISTBOXSEL, to the client window and returns TRUE. This prevents the system from dismissing the dialog box.

If the user presses the CANCEL button, the dialog box is destroyed, using WinDismissDlg. Also, a UM_LISTBOXSEL message is sent to reset the LISTBOXINFO structure and repaint the client window area.
 

Processing the UM_SELECTBOXSEL Message

            SHORT            sSelect = 0;
            SHORT            sIndex = LIT_FIRST;
            HWND             hwndDlg;
            USHORT           i;

            /***************************************************/
            /* first set all to unselected                     */
            /***************************************************/

            for (i = 0; i < NUM_ENTRIES; i++)
               pliInfo->ausListBoxSel[i] = FALSE;

            hwndDlg = WinWindowFromID(HWND_DESKTOP,
                                      IDD_LISTBOX);

            /***************************************************/
            /* get selected items from listbox                 */
            /***************************************************/

            while (sSelect != LIT_NONE && hwndDlg)
            {

               sSelect = (SHORT)WinSendDlgItemMsg(hwndDlg,
                                                  IDL_LISTBOX,
                                                  LM_QUERYSELECTION,
                                                  MPFROMSHORT (sIndex),
                                                  MPVOID);

               pliInfo->ausListBoxSel[sSelect] = TRUE;

               /************************************************/
               /* set query to start at last selected item     */
               /************************************************/

               sIndex = sSelect;
            }

            /***************************************************/
            /* invalidate the window                           */
            /***************************************************/

            WinInvalidateRect(hwndClient,
                              NULL,
                              FALSE);
            break;


When the client window receives the UM_LISTBOXSEL message, it is the client's job to find the selected list box items. Our list box has style LS_MULTIPLESEL, so the user can select as many items as he or she wants. Because so many items can be selected, the procedure to find all of them can be a little tricky; not difficult, just tricky. The message LM_QUERYSELECTION starts at the list box item specified in mpParam1 and returns the first selected item it finds. This is a fairly simple procedure to code. A while loop continues searching until sSelect equals LIT_NONE (in other words, no more items are selected). We next send a LM_QUERYSELECTION message to the list box window, with the variable sIndex indicating the index of the item at which to start the search. At the start of the loop, this variable is LIT_FIRST, the first item in the list box. When the first selected item is found, the variable sSelect contains the index of th item.  As the loop traverses through the items in the list box, the starting search point is updated to sSelect.  As a selected item is found, the corresponding index in the array ausListBoxSel[] is set to TRUE. This information is used in the WM_PAINT processing.

The Client Window Painting Routine

The WM_PAINT processing is where the items selected in the list box actually are written to the client area window.  WinFillRect fills the drawing region with the color CLR_WHITE.
             bReturn = WinQueryWindowRect(hwndClient,
                               &rclPaintRegion);

            rclNewPaint.xLeft = (rclPaintRegion.xRight-
               rclPaintRegion.xLeft)  / 4 * 3;
            rclNewPaint.xRight = rclPaintRegion.xRight;

            rclNewPaint.yBottom = rclPaintRegion.yBottom;
            rclNewPaint.yTop = rclPaintRegion.yTop;

            WinFillRect(hpsPresentationSpace,
                        &rectInvalidRect,
                        CLR_WHITE);

If the use has selected some items, WinDrawText is used to write a heading on the client area. The array ausListBoxSel[] is cycled through to find each selected item and write the list box item text to the client area as well.

Owner-Drawing Controls

An owner-draw style can be used for many of the Presentation Manager controls. This style sends aWM_DRAWITEM message when some portion of the control is to be drawn. This feature lets the programmer customize the appearance of the control.
The LISTBOX example program creates an owner-drawn list box that has system bitmaps and their titles as the selectable items.

LISTBOX.C
LISTBOX.RC
LISTBOX.H
LISTBOX.MAK
LISTBOX.DEF 

The beginning of the program should look familiar. The structure BITMAPDATA is defined:

typedef struct _BITMAPDATA
{
   CHAR             achName[20];
   USHORT           usNumber;
} BITMAPDATA,*PBITMAPDATA;

The first field, achName, is the #define'd text string of each system bitmap. The second field, usNumber, is the number of the system bitmap. When we draw the bimaps, we'll use this structure to access the bitmaps we want.

DlgProc

            for (usIndex = 0; usIndex < MAX_BITMAPS; usIndex++)
            {
               WinSendDlgItemMsg(hwndDlg,
                                 IDL_LISTBOX,
                                 LM_INSERTITEM,
                                 MPFROMSHORT(usIndex),
                                 MPFROMP(""));
            }                          /* endfor               */

The WM_INITDLG message is where the initialization of the dialog box and all its components takes place.  In this case,  we want to initialize the list box. WinSendDlgItemMsg can be used to communicate directly with it. The message LM_INSERTITEM is used to insert items into list box. If this was not an owner-drawn list box, the actual text strings would be inserted here; however, because this is an owner-drawn list box, it is important to tell the list box there will be eight items. The message LM_SELECTITEM is used to set first item to the selected state.

The WM_MEASUREITEM Message

            for (usIndex = 0; usIndex < MAX_BITMAPS; usIndex++)
            {
               hbmBitmap = WinGetSysBitmap(HWND_DESKTOP,
                                           abdBitmaps[usIndex].
                                              usNumber);

               bmihHeader.cbFix = 16;
               GpiQueryBitmapInfoHeader(hbmBitmap,
                                        &bmihHeader);

               /************************************************/
               /* which is larger, previous max or bitmap      */
               /************************************************/

               lMaxCy = max(lMaxCy,
                            bmihHeader.cy);

               /************************************************/
               /* free the bitmap                              */
               /************************************************/

               GpiDeleteBitmap(hbmBitmap);
            }                          /* endfor               */
            return  MRFROMLONG(lMaxCy+10);


The WM_MEASUREITEM message must be processed for an owner-drawn list box and also for horizontal scrolling list boxes. This message tells the list box how tall or, in some cases, how wide each list box item is to be. The tallest, or widest, size should be returned in order for all the list box items to have a consistent look. In our example, all items are the same size. GpiQueryFontMetrics is used to get all sorts of information about the selected font. The one piece of the FONTMETRICS structure we are interested in is fm.Metrics.lMaxBaselineExt. This indicates the maximum height of the font. This is compared to the maximum height of the system bitmap. This information is contained in the BITMAPINFOHEADER structure that is obtained using GpiQueryBitmapInfoHeader. After the comparison, we free the bitmap handle with GpiDeleteBitmap.

The WM_DRAWITEM Message

The WM_DRAWITEM is the most complicated message processing in this example. This message is sent to the owner that will be doing the drawing whenever an item needs to be selected, unselected, or drawn. The second parameter in the WM_DRAWITEM message is a pointer to an OWNERITEM structure, which looks like this:
      typedef struct _OWNERITEM    /* oi */
      {
         HWND    hwnd;
         HPS     hps;
         ULONG   fsState;
         ULONG   fsAttribute;
         ULONG   fsStateOld;
         ULONG   fsAttributeOld;
         RECTL   rclItem;
         LONG    idItem; /* This field contains idItem for menus, iItem for lb. */
         ULONG   hItem;
      } OWNERITEM;
      typedef OWNERITEM *POWNERITEM;

This structure has pretty much everything you need to draw a list box item.

An Introduction to Owner-drawn States

The OWERITEM structure contains the variabes fsState and fsStateOld.  The state variables indicate whether an item needs selection highlighting.  When an item's selection highlighting is changing, the item needs to be redrawn, and the fsState field will be set differently from the fsStateOld field. A state of TRUE indicates the item is selected; FALSE indicates an unselected item. Programmers can draw the highlighting themselves or let the system handle the highlighting and unhighlighting. The flowchart depicted in Figure 15.2 lists the possible combination of states and returns and the action by both the program and the system.
 
Flowchart of owner-drawn selection
Figure 15.2 Flowchart of owner-drawn selection
The system sets these variables before the WM_DRAWITEM message is sent; it looks at what is returned in them after the WM_DRAWITEM message has been processed to determine whether to handle the   highlighting of the item. If fsState is equal to fsStateOld, the system will do no highlighting. If the variables are not equal to each other, the system will highlight them or unhighlight them by inverting the item rectangle.

Drawing the List Box Labels

            poiItem = (POWNERITEM)PVOIDFROMMP(mpParm2);
            rclText = poiItem->rclItem;
            rclText.xLeft = (rclText.xRight-rclText.xLeft)/7;
            /***************************************************/
            /* draw the bitmap name                            */
            /***************************************************/
            WinDrawText(poiItem->hps,
                        -1,
                        abdBitmaps[poiItem->idItem].achName,
                        &rclText,
                        poiItem->fsState?CLR_YELLOW:CLR_BLUE,
                        poiItem->fsState?CLR_BLUE:CLR_WHITE,
                        DT_LEFT|DT_VCENTER|DT_ERASERECT);

A pointer to OWNERITEM structure is contained in mpParm2. The rclItem field is the RECTL structure of the specific list box item that needs to be drawn. We indent the text one-seventh of the way across and use the function WinDrawText to write the bitmap name. Notice the use of the flag DT_ERASERECT in the last parameter. This flag erases the drawing area before Presentation Manager draws the text.
 

Drawing the Bitmaps

            rclText = poiItem->rclItem;
            rclText.xRight = (rclText.xRight-rclText.xLeft)/7;
/* fill the rectangle with white                   */
            WinFillRect(poiItem->hps,
                        &rclText,
                        CLR_WHITE);

            hbmBitmap = WinGetSysBitmap(HWND_DESKTOP,
                                        abdBitmaps[poiItem->
                                           idItem].usNumber);
/* draw the bitmap, then delete                    */
            Draw1Bitmap(poiItem->hps,
                        hbmBitmap,
                        &rclText);

            GpiDeleteBitmap(hbmBitmap);


The next thing to do is get a handle to the bitmap we want to draw in our list box item.  WinGetSysBitmap is used to do this. The first parameter is the desktop window handle, HWND_DESKTOP. The second parameter is the system bitmap number. poiItem->idItem is the index of the selected item. We use this index as the index into the abdBitmaps structure. Draw1Bitmap is a very simple user-defined function we use to actually draw the bitmap. Once bitmap has been drawn, some cleanup will be necessary. The handle of the bitmap needs to be freed using GpiDeleteBitmap.
            poiItem->fsState = FALSE;
            poiItem->fsStateOld = FALSE;
            return  MRFROMSHORT(TRUE);

The last step in our message processing is to set  all the appropriate variables correctly for the window procedure. We set fsState and fsStateOld to FALSE to tell the system we already have done highlighting. A return code of TRUE indicates that the item has been drawn already, so please do not draw it again. If FALSE had been returned here, the text " ", the string that was used in the LM_INSERTITEM message, would be placed over all wonderful work we've done so far.

For more information on drawing bitmaps, see Chapter 12.

Summary

A list box is a very simple control to use, yet it provides a powerful level of functionality. This chapter has introduced the concepts of a regular list box and owner-drawn list box. Developers interested in creating their own, even more advanced list box, should refer to the series of articles by Mark Benge and Matt Smith starting in the January/February 1994 OS/2 Developer magazine.

[ Next ] [ Previous ]     Chapter 15
[ Contents ]  [ Chapter 14: Menus ] [ Chapter 16: Buttons ]