LDC #88: Dialog Boxes Part IV — Combo Boxes

A combo box is a great tool to allow users to select options. Combo boxes, as the name implies, combine the functionality of an edit control with that of a list box. In this article we will look at the functionality of combo boxes and explore their implementations.

Introduction
While a combo is a combination of a list box and an edit control, there are a number of variations. For example, the list box can be hidden when not in use or the edit control can be read only. In some cases, the combo box is a good choice to replace radio style buttons for a long list of options or if the list is subject to change. Control type selection can also be based on style considerations. No matter what, the combo box is a useful tool.
At the outset, it’s worth nothing that the common control for combo box and list box are extremely similar and can be easily confused. Be careful about mixing functions, styles, and messages between the two.

Since they are so similar, our example script is a variation on my last article for list boxes but tailored for a combo box:
Sample Dialog
The example is shown with the dropdown displayed (normally it does not appear until prompted by the user). In the test code, the type of combo box can be changed by uncommenting/commenting control statement lines.
Our test code is as follows:

//
//      Simple Combo Box Test 
//      --------------------- 
// 

#define CB_LIST                         101
#define CB_LOAD                         301
#define CB_FOLDER                       302
#define CB_GET_WIDTH                    303
#define CB_SET_WIDTH                    304
#define CB_SHOW_HIDE                    305
#define CB_RESET                        306
#define CB_INSERT                       307
#define CB_DELETE                       308
#define CB_SELECT                       309
#define CB_SEL_RESET                    310
#define CB_INDEX                        401
#define CB_CONTENT                      402
#define CB_TOTAL_ITEMS                  403

#beginresource
ComboBoxTest01 DIALOGEX 0, 0, 250, 155
EXSTYLE WS_EX_DLGMODALFRAME
STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION
CAPTION "Combo Box Test"
FONT 8, "MS Shell Dlg"
{
 CONTROL "Control", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 6, 5, 26, 8, 0
 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 36, 10, 210, 1, 0
// CONTROL "", CB_LIST, "combobox", CBS_SIMPLE | WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP, 12, 19, 120, 69, 0
 CONTROL "", CB_LIST, "combobox", CBS_DROPDOWN | CBS_SORT | WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP, 12, 19, 120, 69, 0
// CONTROL "", CB_LIST, "combobox", CBS_DROPDOWNLIST | CBS_SORT | WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP, 12, 19, 120, 69, 0
 CONTROL "Load", CB_LOAD, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 143, 15, 45, 12, 0
 CONTROL "Folder", CB_FOLDER, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 143, 30, 45, 12, 0
 CONTROL "Get Width", CB_GET_WIDTH, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 143, 46, 45, 12, 0
 CONTROL "Set Width", CB_SET_WIDTH, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 143, 62, 45, 12, 0
 CONTROL "Show/Hide", CB_SHOW_HIDE, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 143, 78, 45, 12, 0
 CONTROL "Reset", CB_RESET, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 195, 15, 45, 12, 0
 CONTROL "Insert", CB_INSERT, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 195, 30, 45, 12, 0
 CONTROL "Delete", CB_DELETE, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 195, 46, 45, 12, 0
 CONTROL "Select", CB_SELECT, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 195, 62, 45, 12, 0
 CONTROL "Sel Reset", CB_SEL_RESET, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 195, 78, 45, 12, 0
 CONTROL "Index:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 95, 30, 8, 0
 CONTROL "", CB_INDEX, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 43, 95, 200, 8, 0
 CONTROL "Content:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 105, 30, 8, 0
 CONTROL "", CB_CONTENT, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 43, 105, 200, 8, 0
 CONTROL "Total Items:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 115, 30, 8, 0
 CONTROL "", CB_TOTAL_ITEMS, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 43, 115, 200, 8, 0
 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 8, 129, 239, 1, 0
 CONTROL "OK", IDOK, "BUTTON", BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 141, 135, 50, 14
 CONTROL "Cancel", IDCANCEL, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 197, 135, 50, 14
}
#endresource


                                                                /************************************************/

                                                                        /****************************************/
                                                                        /* ** Prototypes                        */
    void                combo_update                    ();             /* Our Support for Dialog Status        */
                                                                        /*                                      */
                                                                        /* ** Static Data                       */
    int                 s_ix;                                           /* Persistent Select Index              */
    int                 drop;                                           /* Drop Toggle Flag                     */


                                                                /************************************************/

                                                                        /****************************************/
                                                                        /* Program Entry                        */
                                                                        /****************************************/

int main () {
    DialogBox("ComboBoxTest01", "combo_");
    return ERROR_NONE;
    }


                                                                /************************************************/

                                                                        /****************************************/
                                                                        /* Dialog Service - Load                */
                                                                        /****************************************/

int combo_load() {
    ComboBoxAddItem(CB_LIST, "Item 1 - Cats");
    ComboBoxAddItem(CB_LIST, "Item 2 - Dogs");
    ComboBoxAddItem(CB_LIST, "Item 3 - Ferrets");
    ComboBoxAddItem(CB_LIST, "Item 4 - Mice");
    combo_update();
    ComboBoxSetEditText(CB_LIST, "Edit Text Area");
    return ERROR_NONE;
    }
        
                                                                        /****************************************/
                                                                        /* Dialog Service - Action              */
                                                                        /****************************************/
void combo_action(int c_id, int c_ac) {
    string              s1;
    int                 rc, width;


    if (c_id == CB_LIST) {                                              // Combo Box
      if (c_ac == CBN_SELCHANGE) {
        combo_update();
        return ;
        }          
      return ;
      }

    if (c_id == CB_LOAD) {                                              // Load
      s1 = ComboBoxGetEditText(CB_LIST);
      if (s1 == "") {
        s1 = "A1, A2 B, A3 CC, A4 DDD, A5 E E E, A6 END";
        }
      ComboBoxReset(CB_LIST);
      ComboBoxLoadList(CB_LIST, s1);
      combo_update();
      EditSetText(CB_CONTENT, s1);
      return ;
      }

    if (c_id == CB_FOLDER) {                                            // Load Folder
      s1 = ComboBoxGetEditText(CB_LIST);
      if (s1 == "") {
        s1 = "C:\\Windows\\*.*";
        }
      ComboBoxReset(CB_LIST);
      ComboBoxLoadFolder(CB_LIST, s1, DDL_ARCHIVE);
      combo_update();
      EditSetText(CB_CONTENT, s1);
      return ;
      }

    if (c_id == CB_GET_WIDTH) {                                         // Get Width
      width = ComboBoxGetDropWidth(CB_LIST);
      s1 = FormatString("Drop Width = %d / %08X", width, width);
      EditSetText(CB_CONTENT, s1);
      return ;
      }

    if (c_id == CB_SET_WIDTH) {                                         // Set Width
      width = ComboBoxGetDropWidth(CB_LIST);
      width += width / 2;
      ComboBoxSetDropWidth(CB_LIST, width);
      return ;
      }

    if (c_id == CB_SHOW_HIDE) {                                         // Drop Up/Down
      rc = 0;
      while (rc < 10) {
        drop ^= 1;
        ComboBoxShowDropDown(CB_LIST, drop);
        rc++;
        s1 = FormatString("Floppy = %d of 10", rc);
        EditSetText(CB_CONTENT, s1);
        Sleep(500);
        }
      EditSetText(CB_CONTENT, "Done");
      return ;
      }

    if (c_id == CB_RESET) {                                             // Reset
      ComboBoxReset(CB_LIST);
      combo_update();
      return ;
      }

    if (c_id == CB_INSERT) {                                            // Insert
      rc = ComboBoxGetItemCount(CB_LIST);
      s1 = FormatString("Inserted at Position 1 (%d items)", rc);
      ComboBoxInsertItem(CB_LIST, 1, s1);
      combo_update();
      return ;
      }

    if (c_id == CB_DELETE) {                                            // Delete
      s_ix = ComboBoxGetSelectIndex(CB_LIST);
      if (s_ix < 0) {
        MessageBox('X', "Nothing selected");
        }
      ComboBoxDeleteItem(CB_LIST, s_ix);
      combo_update();
      return ;
      }

    if (c_id == CB_SELECT) {                                            // Select
      ComboBoxSelectItem(CB_LIST, s_ix);
      s_ix++;
      combo_update();
      return ;
      }

    if (c_id == CB_SEL_RESET) {                                         // Select Reset
      s_ix = 0;
      s_ix--;           // fix -1
      ComboBoxSelectItem(CB_LIST, s_ix);
      combo_update();
      return ;
      }
    }

                                                                        /****************************************/
                                                                        /* Dialog Support                       */
                                                                        /****************************************/
void combo_update() {
    string              s1;
    int                 ix, ic;

    ix = ComboBoxGetSelectIndex(CB_LIST);
    ic = ComboBoxGetItemCount(CB_LIST);
    if (ix < 0) {
      EditSetText(CB_INDEX, "(not selected)");
      EditSetText(CB_CONTENT, "(no data)");
      ControlDisable(CB_DELETE);
      }
    else {
      EditSetText(CB_INDEX, ix);
      s1 = ComboBoxGetItemText(CB_LIST, ix);
      EditSetText(CB_CONTENT, s1);
      ControlEnable(CB_DELETE);
      }
    EditSetText(CB_TOTAL_ITEMS, ic);
    }

Like last time, we have some other controls on the dialog to interact with the combo box and help explore its capabilities. Since the combo box has an edit control, we can enter a list of options that can be loaded into the list portion of the control. In addition, we have a multitude of buttons (“Load”, “Insert”, “Select”, etc) that also perform varied operations with and on the combo box.

The Combo Box Resource

The combo box is created using the conventional CONTROL dialog resource statement with the common control class ‘combobox’. In our example, there are three versions that can be commented and uncommented to explore their behavior.

CONTROL "", CB_LIST, "combobox", CBS_DROPDOWN | CBS_SORT | WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP, 12, 19, 120, 69, 0

Like other dialog controls defined with the CONTROL statement, the initial text is set, followed by the 16-bit control ID, then style flags, position, size, and finally extended style information. The initial text is irrelevant and ignored by the control.

The position and size are in dialog unit as x, y, w, and h. The height can be either the height for a simple combo or maximum height of the combo box for drop-down types. The size of the edit field cannot be directly controlled.

There are three types of combo boxes: simple, drop-down and drop-down list:
Combobox Types
With the simple mode, the edit control and list box are grouped. The second is a variation with the drop arrow allowing for the list box to be shown. In both these examples, the edit control is enabled for data entry. The last mode, drop-down list, only allows items to be selected from the dropdown list (the example does not contain a valid selection). In all three modes, the up/down arrows will move through the list box selecting an item and placing a copy into the edit control. Notice that the height of the left example is larger that the loaded list. As mentioned above, when in simple mode the height specifies the overall display size. While in drop-down mode, however, it is the maximum size of the drop.

Let’s look at some of our available bitwise style options:

Constant Description
CBS_AUTOHSCROLL Automatically scrolls the text in an edit control to the right when the user types a character at the end of the line. If this style is not set, only text that fits within the rectangular boundary is allowed.
CBS_DISABLENOSCROLL Shows a disabled vertical scrollbar in the list box when the box does not contain enough items to scroll. Without this style, the scrollbar is hidden when the list box does not contain enough items.
CBS_DROPDOWN Similar to CBS_SIMPLE, except that the list box is not displayed unless the user selects an icon next to the edit control. This is the center example above.
CBS_DROPDOWNLIST Similar to CBS_DROPDOWN, except that the edit control is replaced by a static text item that displays the current selection in the list box. This is the right example above.
CBS_LOWERCASE Converts all text in both the selection field and the list to lowercase.
CBS_NOINTEGRALHEIGHT Specifies that the size of the combo box is exactly the size specified by the application when it created the combo box. Normally, the system sizes a combo box so that it does not display partial items.
CBS_SIMPLE Displays the list box at all times. The current selection in the list box is displayed in the edit control. This is the left example above.
CBS_SORT Automatically sorts strings added to the list box.
CBS_UPPERCASE Converts all text in both the selection field and the list to uppercase.

 
In addition to the above bits, the standard window styles also apply, such as WS_TABSTOP and WS_VSCROLL. A horizontal scroll bar is not supported.

Loading Data

Since a combo box is two controls in one, we need to look at both the edit control and the list box section. Let’s start with the list box section which, of course, operates much like a list box.

Part A — The List

There are multiple methods of loading data into the list portion. Obviously one can add or insert items one by one:
int = ComboBoxAddItem ( int id, mixed value );
or
int = ComboBoxInsertItem ( int id, int index, mixed value );
In both routines, the id parameter specifies the control ID. For insert, the index parameter specifies the index at which to insert the data. For example, passing the function a value of 0 will insert the string at the top of the list. Inserting a string does not re-sort the list, while adding a string does sort the list with every addition. For insert, an index of -1 will insert the string at the end of the list. The value parameter can be any non-scalar value except a handle.

Both the ComboBoxAddItem and ComboBoxInsertItem functions are demonstrated in the above sample: add on load and add and insert when the Insert button is pressed.

For lists of constants, the ComboBoxLoadList function works well:
int = ComboBoxLoadList ( int id, string data );
The data parameter string can be in one of three forms: lines, comma separated text, or space separated text. The type of data is automatically selected based on the detected content. Pressing the Load button will either load the data currently located in the edit control to the list box, or, if the edit control is empty, the list will be populated with default data.

An array or list of data can be added using the ComboBoxAddArray function:
int = ComboBoxAddArray ( int id, string array );
Each array item is added to the list. If CBS_SORT is enabled, the resulting list is sorted.

The last item for the list box portion is the ability to load a folder’s contents using the ComboBoxLoadFolder function. The Folder button demonstrates loading a folder. You can type a qualified path into the edit control above the list box and press the Folder button to populate the list box with the items in that folder. Leaving the edit control empty loads the Windows folder.

Part B — The Edit Control

For simple and drop-down style boxes, the edit portion of the combo box operates like an ‘edit’ class common control except that when and item is selected by the user, the content of the control will be loaded with a copy of the selection. For drop-down lists, the control is ‘static’ and can only show the selection or empty.

To load the content of the edit control:
int = ComboBoxSetEditText ( int id, string text );

Text will only be set if the style is not CBS_DROPDOWNLIST. To set the static control, an item in the list must be selected:
int = ComboBoxSelectItem ( int id, [int index | string data] );
We will return to this function when we discuss selecting items in the list.

Reading List Data

To retrieve data from the edit portion:
string = ComboBoxGetEditText ( int id );
Like setting text in the edit field portion, this works on simple and drop-down styles. Since the edit portion is a common control, the EditGetText function can be used to perform additional validation. If an attempt is made to retrieve text from a drop-down list, the function will return an empty string without an indication of error.

Items can also be read from the list part of the control. While reading the list is not generally required for this type of control, it’s useful to know the option to do it is there. To retrieve a single item:
string = ComboBoxGetItemText ( int id, int index );

If you want to know how many items are in the list:
int = ComboBoxGetItemCount ( int id );

Check the result for an error using the IsError function before assuming the value is the count. In our example, the ComboBoxGetItemCount function is used both for displaying status and for the Insert button. Similar to the list box, the ComboBoxGetArray function will retrieve all the items in a string list.

Removing Items

If you want to completely clear the control, the ComboBoxReset function will remove every item in the list and clear the edit portion of the control. This is useful when the list needs to be reloaded. Pressing the Reset button demonstrates the control reset.

If a single it item is to be deleted, the ComboBoxDeleteItem function will remove a specified index. Pressing the Delete button demonstrates the list box delete for the current selection. In the example program, notice deleting also removes the selection. Try adding logic to preserve the selection and even move it if the last item was deleted.

Selecting and Deselecting

The list portion of a combo box operates in the equivalent mode of single select for a list box. Select an item using the following function:
int = ComboBoxSelectItem ( int id, [int index | string data] );

The index is zero-based. To reset or remove the selection, set the index to -1 or an empty string “”. If neither the index or data parameter is provided, the selected item position is reset. String matches are exact, including text case.

A more common action is to read the selection, and for this we use the ComboBoxGetSelectIndex function:
int = ComboBoxGetSelectIndex ( int id );

If an item not is selected, the return value will be -1. (As a side note, there is a known script parsing issue with the unary operator minus, such as -x. As a workaround, place the negative value in parenthesis. For example: if (x == (-1)) { … }

The selection can also be read as a string using the ComboBoxGetSelectString function.

If you need to search for an item in the list, there are two functions to help:
int = ComboBoxFindItem ( int id, string data );

int = ComboBoxFindItemExact ( int id, string data );

The first routine performs a loose search while the second requires a character for character match. If either fails, the result will be -1.

Finally, if the user types in the edit portion, the selection of a list item is reset.

Notifications

The combo box class sends the following notification codes via the action procedure:

Notification code Value Description
CBN_CLOSEUP 8 Indicates the list in a drop-down combo box or drop-down list box is about to close.
CBN_DBLCLK 2 Indicates the user has double-clicked a list item in a simple combo box.
CBN_DROPDOWN 7 Indicates the list in a drop-down combo box or drop-down list box is about to open.
CBN_EDITCHANGE 5 Indicates the user has changed the text in the edit control of a simple or drop-down combo box. This notification code is sent after the altered text is displayed.
CBN_EDITUPDATE 6 Indicates the user has changed the text in the edit control of a simple or drop-down combo box. This notification code is sent before the altered text is displayed.
CBN_ERRSPACE -1 Indicates the combo box cannot allocate enough memory to carry out a request, such as adding a list item.
CBN_KILLFOCUS 4 Indicates the combo box is about to lose the input focus.
CBN_SELCHANGE 1 Indicates the current selection has changed.
CBN_SELENDCANCEL 10 Indicates that the selection made in the drop-down list, while it was dropped down, should be ignored.
CBN_SELENDOK 9 Indicates that the selection made in the drop-down list, while it was dropped down, should be accepted.
CBN_SETFOCUS 3 Indicates the combo box has received the input focus.

 
The most used items are CBN_SELCHANGE and CBN_EDITCHANGE. Selection changes can be used to update data within the dialog. Within the action procedures, we see the following code:

    if (c_id == CB_LIST) { // Combo Box
      if (c_ac == CBN_SELCHANGE) {
        combo_update();
        return ;
        }          
      return ;
      }

The ID is first checked for the list box. Then each of two action notification sub messages are checked and processed. This can be expanded to process multiple sub messages.

The edit change and update can be a bit confusing. Sometimes it is desirable to read the edit portion of the control after a select change. Make sure you read the control after it is updated. Otherwise your program will pick up the data previously in the edit area.

The Drop-Down

There are a couple of functions worth mentioning regarding the drop-down list. The first is expanding the width of the list. Let us add another item into the list box that will not fit:
Sample dialog with cut-off combobox
Notice the text is chopped off. Our choice is to widen the entire control, which isn’t always ideal, or simply widen just the drop-down with this function:

int = ComboBoxSetDropWidth ( int id, int width );
The ComboBoxSetDropWidth function will set the drop list’s width in pixels. But to what? We can also get the width and then do a little math. The Get Width and Set Width buttons demonstrate the functions:

    if (c_id == CB_GET_WIDTH) {        // Get Width
      width = ComboBoxGetDropWidth(CB_LIST);
      s1 = FormatString("Drop Width = %d / %08X", width, width);
      EditSetText(CB_CONTENT, s1);
      return ;
      }

    if (c_id == CB_SET_WIDTH) {        // Set Width
      width = ComboBoxGetDropWidth(CB_LIST);
      width += width / 2;
      ComboBoxSetDropWidth(CB_LIST, width);
      return ;
      }

Pressing the Set Width button will incrementally increase the width by 50%. Here is a modification to perform the width change on load:

int combo_load() {
    int                 w;

    w = ComboBoxGetDropWidth(CB_LIST);
    if (w > 0) {
      ComboBoxSetDropWidth(CB_LIST, w + w / 2);
      }
    ComboBoxAddItem(CB_LIST, "Item 1 - Cats");
    ComboBoxAddItem(CB_LIST, "Item 2 - Dogs");
    ComboBoxAddItem(CB_LIST, "Item 3 - Ferrets");
    ComboBoxAddItem(CB_LIST, "Item 4 - Mice");
    ComboBoxAddItem(CB_LIST, "Item 5 - And a Bunch of Furry Little Things");
    combo_update();
    ComboBoxSetEditText(CB_LIST, "Edit Text Area");
    return ERROR_NONE;
    }

So the width is read from the existing control and expanded 50%:
Sample dialog with wide combobox
Now, if this change is made, we need to add the style bit CBS_AUTOHSCROLL to the control’s definition. Otherwise the text will be clipped in the edit control.

The drop-down can also be shown or hidden by the script using the following function:
int = ComboBoxShowDropDown ( int id, [boolean mode] );

If you press the Show/Hide button, it bounces the control up and down a few times for fun. Still, this can be useful when entering a dialog and wanting to have the list immediately be visible to the user.

Conclusion

Combo boxes are particularly adept for allowing the user to pick data, such as selecting an item from a longer list of options. (If you have ever used the <SELECT> HTML form control, you can see the similarities.)
Next time you are wondering whether a radio button the best choice, or if you have too many items to easily format into a dialog, consider a combo box.
 

Scott Theis is the President of Novaworks and the principal developer of the Legato scripting language. He has extensive expertise with EDGAR, HTML, XBRL, and other programming languages.