For user selection controls within dialog boxes, there are three conventional options: checkboxes, radio buttons and list/combo boxes. Each has its application since each has its strengths and weaknesses. Sometimes developers make poor choices concerning which of the three to use on their on dialogs or web pages. Bad implementations may still work, but they provide a less intuitive user experience, and these uses may not be in the spirit for which the controls were designed. Hopefully this article will help improve your knowledge and therefore ability to choose a selection control through providing information on how each type works.
Friday, March 15. 2019
LDC #127: Checkbox and Radio Button Controls Part 1
Options, Options, Options
So which type is the best? That depends on the situation and what each can do for the programmer and the end user. Let’s examine the differences:
Checkbox — Designed, like a paper form, to check or affirm a condition or option. Like a paper form, a checkbox connotes “Check All That Apply”, meaning more than one item can be checked. In addition, it can make sense to have a single checkbox to toggle an “on/off” or “yes/no” condition. Checkboxes are not initially programmatically interrelated, which means when you place them on a dialog, they operate independently, even if they are visually grouped together.
Radio Button — Designed, like a paper form, to check only one. For example, consider the options “Residence: Single Family Home, Duplex, Multi Unit Home, Townhome or Apartment”. The user is expected to check only one. This is an important distinction from a checkbox.
Combo Box — For Windows (and Legato). Combo boxes are a combination of a list box and an optional text box. They can be set up to allow only selections from a list. In that mode, combo boxes are similar to radio buttons. Other implementations, such as HTML or custom classes, may allow multiple selections, but the display scheme may or may not allow all selections to be seen.
List Box — List boxes can be setup to allow either single or multiple selections.
In reality, there are two implementation scenarios for user selection: allowing the user to pick as many items that apply as desired or permitting the user to select only one item. For the latter case, check boxes and list boxes are particularly good choices for a selection control. This blog will examine the checkbox and list box alternative. The example code shares the same data set to the two programs can be easily compared. Next week will cover the radio button and the combo box option.
Example Checkbox Implementation
For our checkbox example, I have written a short script that can query as user as to his or her allergies. While I am certain there are a lot more allergies, this gives an example of using the checkbox with common allergies.
Checkbox Example
Upon pressing OK, the selections are saved in an INI file and a message box displays the result:
This also provides a demonstration of the storage of information. Of course, there are many other options and techniques. Once you set selections and press OK, the next time the dialog loads, it will remember the previous selections.
The Code
// Resource #beginresource #define CB_BOX_NONE 201 #define CB_BOX_MILK 202 #define CB_BOX_EGG 203 #define CB_BOX_PEANUT 204 #define CB_BOX_TREE_NUT 205 #define CB_BOX_WHEAT 206 #define CB_BOX_SOY 207 #define CB_BOX_FISH 208 #define CB_BOX_SHELLFISH 209 #define CB_BOX_SESAME 210 #define CB_BOX_OTHER 211 #define CB_OTHER_TITLE 212 #define CB_OTHER_TEXT 213 CheckboxExample01Dlg DIALOGEX 0, 0, 240, 130 EXSTYLE WS_EX_DLGMODALFRAME STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Check Box Example" FONT 8, "MS Shell Dlg" { CONTROL "Food Allergies:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 6, 6, 60, 8, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 56, 11, 176, 1 CONTROL "&None", CB_BOX_NONE, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 14, 18, 60, 12, 0 CONTROL "&Milk", CB_BOX_MILK, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 14, 30, 60, 12, 0 CONTROL "&Egg", CB_BOX_EGG, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 84, 30, 60, 12, 0 CONTROL "&Peanut", CB_BOX_PEANUT, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 154, 30, 60, 12, 0 CONTROL "&Tree Nut", CB_BOX_TREE_NUT, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 14, 42, 60, 12, 0 CONTROL "&Wheat", CB_BOX_WHEAT, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 84, 42, 60, 12, 0 CONTROL "&Soy", CB_BOX_SOY, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 154, 42, 60, 12, 0 CONTROL "&Fish", CB_BOX_FISH, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 14, 54, 60, 12, 0 CONTROL "S&hellfish", CB_BOX_SHELLFISH, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 84, 54, 60, 12, 0 CONTROL "Ses&ame", CB_BOX_SESAME, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 154, 54, 60, 12, 0 CONTROL "&Other", CB_BOX_OTHER, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 14, 66, 40, 12, 0 CONTROL "E&xplain:", CB_OTHER_TITLE, "static", WS_CHILD | WS_VISIBLE, 60, 68, 60, 12, 0 CONTROL "", CB_OTHER_TEXT, "edit", ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 60, 82, 160, 12 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 6, 102, 226, 1 CONTROL "OK", IDOK, "button", BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 127, 108, 50, 14, 0 CONTROL "Cancel", IDCANCEL, "button", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 180, 108, 50, 14, 0 } #endresource // Dialog Data string cb_allergies; string cb_other_text; // Main Entry int main() { int rc; cb_allergies = GetSetting("Dialog Examples", "Multiple Select Options", "Allergies"); cb_other_text = GetSetting("Dialog Examples", "Multiple Select Options", "Allergies Other"); rc = DialogBox("CheckboxExample01Dlg", "cb_"); if (IsNotError(rc)) { PutSetting("Dialog Examples", "Multiple Select Options", "Allergies", cb_allergies); PutSetting("Dialog Examples", "Multiple Select Options", "Allergies Other", cb_other_text); MessageBox("Codes: %s\r\rOtherText: %s", cb_allergies, cb_other_text); } return rc; } // In Dialog Proc - Set State void cb_set_state() { if (CheckboxGetState(CB_BOX_NONE)) { ControlDisable(CB_BOX_MILK); ControlDisable(CB_BOX_EGG); ControlDisable(CB_BOX_PEANUT); ControlDisable(CB_BOX_TREE_NUT); ControlDisable(CB_BOX_WHEAT); ControlDisable(CB_BOX_SOY); ControlDisable(CB_BOX_FISH); ControlDisable(CB_BOX_SHELLFISH); ControlDisable(CB_BOX_SESAME); ControlDisable(CB_BOX_OTHER); ControlDisable(CB_OTHER_TITLE); ControlDisable(CB_OTHER_TEXT); } else { ControlEnable(CB_BOX_MILK); ControlEnable(CB_BOX_EGG); ControlEnable(CB_BOX_PEANUT); ControlEnable(CB_BOX_TREE_NUT); ControlEnable(CB_BOX_WHEAT); ControlEnable(CB_BOX_SOY); ControlEnable(CB_BOX_FISH); ControlEnable(CB_BOX_SHELLFISH); ControlEnable(CB_BOX_SESAME); ControlEnable(CB_BOX_OTHER); } if (CheckboxGetState(CB_BOX_OTHER)) { ControlEnable(CB_OTHER_TITLE); ControlEnable(CB_OTHER_TEXT); } else { ControlDisable(CB_OTHER_TITLE); ControlDisable(CB_OTHER_TEXT); } } // In Dialog Proc - Load int cb_load() { if (ScanString(cb_allergies, "None") >= 0) { CheckboxSetState(CB_BOX_NONE); } if (ScanString(cb_allergies, "Milk") >= 0) { CheckboxSetState(CB_BOX_MILK); } if (ScanString(cb_allergies, "Egg") >= 0) { CheckboxSetState(CB_BOX_EGG); } if (ScanString(cb_allergies, "Peanut") >= 0) { CheckboxSetState(CB_BOX_PEANUT); } if (ScanString(cb_allergies, "Tree Nut") >= 0) { CheckboxSetState(CB_BOX_TREE_NUT); } if (ScanString(cb_allergies, "Wheat") >= 0) { CheckboxSetState(CB_BOX_WHEAT); } if (ScanString(cb_allergies, "Soy") >= 0) { CheckboxSetState(CB_BOX_SOY); } if (ScanString(cb_allergies, "Fish") >= 0) { CheckboxSetState(CB_BOX_FISH); } if (ScanString(cb_allergies, "Shellfish") >= 0) { CheckboxSetState(CB_BOX_SHELLFISH); } if (ScanString(cb_allergies, "Sesame") >= 0) { CheckboxSetState(CB_BOX_SESAME); } if (ScanString(cb_allergies, "Other") >= 0) { CheckboxSetState(CB_BOX_OTHER); EditSetText(CB_OTHER_TEXT, cb_other_text); } cb_set_state(); return ERROR_NONE; } // In Dialog Proc - Control Change void cb_action(int c_id, int c_ac) { int state; if (c_id == CB_BOX_NONE) { state = CheckboxGetState(c_id); if (state != FALSE) { CheckboxSetState(CB_BOX_MILK, 0); CheckboxSetState(CB_BOX_EGG, 0); CheckboxSetState(CB_BOX_PEANUT, 0); CheckboxSetState(CB_BOX_TREE_NUT, 0); CheckboxSetState(CB_BOX_WHEAT, 0); CheckboxSetState(CB_BOX_SOY, 0); CheckboxSetState(CB_BOX_FISH, 0); CheckboxSetState(CB_BOX_SHELLFISH, 0); CheckboxSetState(CB_BOX_SESAME, 0); CheckboxSetState(CB_BOX_OTHER, 0); EditSetText(CB_OTHER_TEXT, ""); } } if (c_id == CB_BOX_OTHER) { if (CheckboxGetState(c_id) == FALSE) { EditSetText(CB_OTHER_TEXT, ""); } } if ((c_id == CB_BOX_NONE) || (c_id == CB_BOX_MILK) || (c_id == CB_BOX_EGG) || (c_id == CB_BOX_PEANUT) || (c_id == CB_BOX_TREE_NUT) || (c_id == CB_BOX_WHEAT) || (c_id == CB_BOX_SOY) || (c_id == CB_BOX_FISH) || (c_id == CB_BOX_SHELLFISH) || (c_id == CB_BOX_SESAME) || (c_id == CB_BOX_OTHER)) { cb_set_state(); } } // In Dialog Proc - Pressed OK int cb_validate() { string s1, s2; if (CheckboxGetState(CB_BOX_NONE)) { s1 = AppendWithDelimiter(s1, "None"); } if (CheckboxGetState(CB_BOX_MILK)) { s1 = AppendWithDelimiter(s1, "Milk"); } if (CheckboxGetState(CB_BOX_EGG)) { s1 = AppendWithDelimiter(s1, "Egg"); } if (CheckboxGetState(CB_BOX_PEANUT)) { s1 = AppendWithDelimiter(s1, "Peanut"); } if (CheckboxGetState(CB_BOX_TREE_NUT)) { s1 = AppendWithDelimiter(s1, "Tree Nut"); } if (CheckboxGetState(CB_BOX_WHEAT)) { s1 = AppendWithDelimiter(s1, "Wheat"); } if (CheckboxGetState(CB_BOX_SOY)) { s1 = AppendWithDelimiter(s1, "Soy"); } if (CheckboxGetState(CB_BOX_FISH)) { s1 = AppendWithDelimiter(s1, "Fish"); } if (CheckboxGetState(CB_BOX_SHELLFISH)) { s1 = AppendWithDelimiter(s1, "Shellfish"); } if (CheckboxGetState(CB_BOX_SESAME)) { s1 = AppendWithDelimiter(s1, "Sesame"); } if (CheckboxGetState(CB_BOX_OTHER)) { s1 = AppendWithDelimiter(s1, "Other"); s2 = EditGetText(CB_OTHER_TEXT); if (s2 == "") { MessageBox('X', "Please specify the nature of the other allergy."); return ERROR_SOFT | CB_OTHER_TEXT; } } if (s1 == "") { MessageBox('X', "Select at least one allergy or pick 'None'."); return ERROR_SOFT | CB_BOX_NONE; } cb_allergies = s1; cb_other_text = s2; return ERROR_NONE; }
The program is divided into seven sections: resource declaration, main entry, global data, control state management, load, control action, and validation.
Details on the checkbox CONTROL resource statement is covered below. The dialog itself is basic (a single page, not a property sheet style) dialog. One thing to notice is the volume of code associated with each allergy type. Each one requires a id declaration, control declaration, and associated code to manage it. This adds to the development time and, of course, maintenance if fields are added or removed. Given the number of controls in this example, this is not a big issue here, but obviously with many possible choices, it could become a problem.
The next part contains the global declarations. We have two: a string for a list of allergies and a string for the text pertaining to the “Other” field. Since aside from an error code, data cannot be passed to and from a dialog, global data is the method.
Entry is via the main() function. This function loads previous settings, launches the dialog. If all of its operations complete OK (ERROR_NONE), it then stores the result. Note that the global data is not altered by the dialog procedure unless the validation completes successfully. This is a good practice and makes for a clean cancel operation.
Then we have the dialog procedures. The first, the cb_set_state() function, is actually not an internally defined dialog procedure (i.e., it is not called directly by the internal dialog manager). It actually serves other procedures by setting the state of the controls based on the context of certain other controls. For example, checking ‘None’ should cause everything to become disabled. Uncheck this item, the function will enable most of the controls except for the ‘Other’ text selection, which in turn has its own logic.
The loader is pretty straight forward. It searches for tokens in the allergies string, using the ScanString SDK function, and sets checkboxes as required. This does allow bad data to filter in, such as having ‘None’ checked while having others checked. However, that should never happen under normal circumstances.
Response to control changes is a little more complex. An affirmative to ‘None’ causes all other controls to be cleared. Unchecking ‘Other’ clears the explanation. Any checkbox action results in a state check. Note that in the current resource declaration, the ONLY messages being send by checkboxes are state change messages. If notifications were allowed (BS_NOTIFY), then the action function MUST look at the control action type to respond. In this example, as is commonly implemented, the checkboxes are automatic, meaning that upon a user mouse click (or space bar) the button is toggled between checked and unchecked. This is the BS_AUTOCHECKBOX style. Other styles require the intervention of the action function to determine how to set the state upon a check change.
Finally, pressing the OK button calls the cb_validate() function. It gets the state of each checkbox and adds tokens to a temporary string s1. If ‘Other’ is checked, the ‘Other’ text edit control is tested for a value. The entire resultant string containing the checked allergies is examined to determine if it’s empty (in other words, to make sure the user selected something). Assuming all is okay, the dialog exits.
Checkbox API Functions
This is good place to look at the checkbox API functions. Let’s start with functions pertaining to a checkbox’s state:
int = CheckboxGetState ( int id );
int = CheckboxSetState ( int id, [int state] );
The state parameter values (and returned value) are as follows:
Return code | Code | Description | ||||
BST_CHECKED | 0x00000001 | Button is checked | ||||
BST_INDETERMINATE | 0x00000002 | Button is grayed, indicating an indeterminate state (applies only if the button has the BS_3STATE or BS_AUTO3STATE style). | ||||
BST_UNCHECKED | 0x00000000 | Button is cleared |
For two state checkboxes, the state of the checkbox can be used as a boolean value for conditional statements (i.e., if the checkbox is checked, the following code should be executed). For tri-state checkboxes, you must explicitly test its state against the various cases (checked, unchecked, and indeterminate).
The remaining functions are not used in the example code, but here they are for your edification:
string = CheckboxGetText ( int id );
int = CheckboxSetText ( int id, string text );
These two functions allow the legend (the text attached to the checkbox) to be read and/or set.
The checkbox’s highlight state can also be set:
int = CheckboxHighlight ( int id, boolean state );
Finally, an image can be added if either the BS_ICON or BS_BITMAP styles are set.
int = CheckboxSetImage ( int id, handle hResource | string name );
Checkbox Resource Details
The key to the resource declarations is the CONTROL statement. It operates with the checkbox class as it does for any other class. We are selecting the Windows SDK common control “button”. It is made into a checkbox by the use of the BS_AUTOCHECKBOX style.
CheckboxExample01Dlg DIALOGEX 0, 0, 240, 130 EXSTYLE WS_EX_DLGMODALFRAME STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Check Box Example" FONT 8, "MS Shell Dlg" { CONTROL "Food Allergies:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 6, 6, 60, 8, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 56, 11, 176, 1 CONTROL "&None", CB_BOX_NONE, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 14, 18, 60, 12, 0 CONTROL "&Milk", CB_BOX_MILK, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 14, 30, 60, 12, 0 CONTROL "&Egg", CB_BOX_EGG, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 84, 30, 60, 12, 0 CONTROL "&Peanut", CB_BOX_PEANUT, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 154, 30, 60, 12, 0 CONTROL "&Tree Nut", CB_BOX_TREE_NUT, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 14, 42, 60, 12, 0 CONTROL "&Wheat", CB_BOX_WHEAT, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 84, 42, 60, 12, 0 CONTROL "&Soy", CB_BOX_SOY, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 154, 42, 60, 12, 0 CONTROL "&Fish", CB_BOX_FISH, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 14, 54, 60, 12, 0 CONTROL "S&hellfish", CB_BOX_SHELLFISH, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 84, 54, 60, 12, 0 CONTROL "Ses&ame", CB_BOX_SESAME, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 154, 54, 60, 12, 0 CONTROL "&Other", CB_BOX_OTHER, "button", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 14, 66, 40, 12, 0 CONTROL "E&xplain:", CB_OTHER_TITLE, "static", WS_CHILD | WS_VISIBLE, 60, 68, 60, 12, 0 CONTROL "", CB_OTHER_TEXT, "edit", ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 60, 82, 160, 12 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 6, 102, 226, 1 CONTROL "OK", IDOK, "button", BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 127, 108, 50, 14, 0 CONTROL "Cancel", IDCANCEL, "button", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 180, 108, 50, 14, 0 }
Control declarations have the following form:
CONTROL text, id, class, style, x, y, width, height, [extended-style]
The text parameter is the legend (text) of the control. Note the use of ‘&‘ prefixes to create keyboard shortcuts. Controls are referenced in the code via the 16-bit id parameter. The main part of the control’s behavior is set by the style parameter which has two parts: basic Windows SDK window bits and bits specific to the class, WS_ and BS_ prefixes, respectively.
Checkbox specific button styles are as follows:
Constant | Description | |||
---|---|---|---|---|
BS_3STATE | Creates a button that is the same as a checkbox, except that the box can be grayed as well as checked or cleared. Use the grayed state to show that the state of the checkbox is indeterminate. | |||
BS_AUTO3STATE | Creates a button that is the same as a three-state check box, except that the box changes its state when the user selects it. The state cycles through checked, indeterminate, and cleared. | |||
BS_AUTOCHECKBOX | Creates a button that is the same as a checkbox, except that the check state automatically toggles between checked and cleared each time the user selects the check box. | |||
BS_BITMAP | Specifies that the button displays a bitmap. | |||
BS_CHECKBOX | Creates a small, empty checkbox with text. By default, the text is displayed to the right of the check box. To display the text to the left of the check box, combine this flag with the BS_LEFTTEXT style (or with the equivalent BS_RIGHTBUTTON style). | |||
BS_ICON | Specifies that the button displays an icon. | |||
BS_NOTIFY | Enables a button to send BN_KILLFOCUS and BN_SETFOCUS notification codes to its parent window. Note that buttons send the BN_CLICKED notification code regardless of whether it has this style. To get BN_DBLCLK notification codes, the button must have the BS_RADIOBUTTON or BS_OWNERDRAW style. | |||
BS_PUSHLIKE | Makes a button (such as a checkbox, three-state checkbox, or radio button) look and act like a push button. The button looks raised when it isn’t pushed or checked and sunken when it is pushed or checked. |
The position parameters are in dialog units with 0,0 being in the upper left of the client area of the dialog page.
Notifications
In our example, we are assuming that every notification is a click. If BS_NOTIFY is set, then a number of actions will be dispatched:
Define | Code | Description | ||||
BN_CLICKED | 0 | Sent when the user clicks a button. | ||||
BN_HILITE | 2 | Sent when the user selects a button. | ||||
BN_UNHILITE | 3 | Sent when the highlight should be removed from a button. This is used principally in owner drawn controls. | ||||
BN_DISABLE | 4 | Sent when a button is disabled. This is used principally in owner drawn controls. | ||||
BN_DOUBLECLICKED | 5 | Sent when the user double-clicks a button. This notification code is sent automatically for BS_USERBUTTON, BS_RADIOBUTTON, and BS_OWNERDRAW buttons. Other button types send BN_DOUBLECLICKED only if they have the BS_NOTIFY style. | ||||
BN_PUSHED | BN_HILITE | Sent when the push state of a button is set to pushed. This is used principally in owner drawn controls. | ||||
BN_UNPUSHED | BN_UNHILITE | Sent when the push state of a button is set to unpushed. This is used principally in owner drawn controls. | ||||
BN_DBLCLK | BN_DOUBLECLICKED | Same as BN_DOUBLECLICKED. | ||||
BN_SETFOCUS | 6 | Sent when a button receives the keyboard focus. The button must have the BS_NOTIFY style to send this notification code. | ||||
BN_KILLFOCUS | 7 | Sent when a button loses the keyboard focus. The button must have the BS_NOTIFY style to send this notification code. |
List Box Implementation
I covered the operation of the list box control in LDC #84: Dialog Boxes Part III — List Boxes. There is more information on functions and styles in that article. In this section I will cover only those things that apply to the multiple item selection paradigm to make it comparable to checkbox functionality.
List Box Example
Let’s begin by looking at our list box example matching the functionality of the check box example:
The first thing you will notice is that as a stand along option, the list box method makes the dialog look lopsided. We will ignore that for the sake of comparison. In certain dialogs, this design makes sense (we used this method in an early product for EDGAR SROS selections). A list box with multiple select style, LBS_MULTIPLESEL, is employed:
The Code
Like the checkbox example, the code is divided into seven sections: resource declaration, main entry, global data, control state management, load, control action, and validation. As you will see, there is a lot less code in this example.
// Resource #beginresource #define LB_SELECTIONS 201 #define LB_OTHER_TITLE 212 #define LB_OTHER_TEXT 213 ListBoxExample01Dlg DIALOGEX 0, 0, 240, 150 EXSTYLE WS_EX_DLGMODALFRAME STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "List Box Example" FONT 8, "MS Shell Dlg" { CONTROL "Food &Allergies:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 6, 6, 60, 8, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 56, 11, 176, 1 CONTROL "", LB_SELECTIONS, "listbox", LBS_NOTIFY | LBS_MULTIPLESEL | LBS_NOINTEGRALHEIGHT | WS_BORDER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 14, 19, 40, 92 CONTROL "Other E&xplain:", LB_OTHER_TITLE, "static", WS_CHILD | WS_VISIBLE, 65, 85, 60, 12, 0 CONTROL "", LB_OTHER_TEXT, "edit", ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 65, 98, 160, 12 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 6, 120, 226, 1 CONTROL "OK", IDOK, "button", BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 127, 126, 50, 14, 0 CONTROL "Cancel", IDCANCEL, "button", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 180, 126, 50, 14, 0 } #endresource // Dialog Data string lb_allergies; string lb_other_text; #define MAX_ALLERGY_ITEMS 20 // Main Entry int main() { int rc; lb_allergies = GetSetting("Dialog Examples", "Multiple Select Options", "Allergies"); lb_other_text = GetSetting("Dialog Examples", "Multiple Select Options", "Allergies Other"); rc = DialogBox("ListBoxExample01Dlg", "lb_"); if (IsNotError(rc)) { PutSetting("Dialog Examples", "Multiple Select Options", "Allergies", lb_allergies); PutSetting("Dialog Examples", "Multiple Select Options", "Allergies Other", lb_other_text); MessageBox("Codes: %s\r\rOtherText: %s", lb_allergies, lb_other_text); } return rc; } // In Dialog Proc - Set State void lb_set_state() { string list[MAX_ALLERGY_ITEMS]; list = ListBoxGetSelectArray(LB_SELECTIONS); if (FindInList(list, "Other") >= 0) { ControlEnable(LB_OTHER_TITLE); ControlEnable(LB_OTHER_TEXT); } else { ControlDisable(LB_OTHER_TITLE); ControlDisable(LB_OTHER_TEXT); } } // In Dialog Proc - Load int lb_load() { string list[MAX_ALLERGY_ITEMS]; int lx, ix; ListBoxLoadList(LB_SELECTIONS, "None, Milk, Egg, Peanut, Tree Nut, Wheat, Soy, Fish, Shellfish, Sesame, Other"); if (lb_allergies != "") { list = ExplodeString(lb_allergies, ", "); while (list[ix] != "") { lx = ListBoxFindItem(LB_SELECTIONS, list[ix]); if (lx >= 0) { ListBoxSelectItem(LB_SELECTIONS, lx); } ix++; } } EditSetText(LB_OTHER_TEXT, lb_other_text); lb_set_state(); return ERROR_NONE; } // In Dialog Proc - Control Change void lb_action(int c_id, int c_ac) { if ((c_id == LB_SELECTIONS) && (c_ac == LBN_SELCHANGE)) { lb_set_state(); } } // In Dialog Proc - Pressed OK int lb_validate() { string list[MAX_ALLERGY_ITEMS]; string s1, s2; list = ListBoxGetSelectArray(LB_SELECTIONS); if (ArrayGetAxisDepth(list) != 0) { s1 = ImplodeArray(list, ", "); } if (ScanString(s1, "Other") >= 0) { s2 = EditGetText(LB_OTHER_TEXT); if (s2 == "") { MessageBox('X', "Please specify the nature of the other allergy."); return ERROR_SOFT | LB_OTHER_TEXT; } } if (s1 == "") { MessageBox('X', "Please make an allergy selection or 'None'."); return ERROR_SOFT | LB_SELECTIONS; } if ((ScanString(s1, "None") >= 0) && (s1 != "None")) { MessageBox('X', "Other allergies cannot be selected with 'None'."); return ERROR_SOFT | LB_SELECTIONS; } lb_allergies = s1; lb_other_text = s2; return ERROR_NONE; }
Skipping to the state management, we are primarily interested enabling and disabling the ‘other’ items. Each time there is a change, the list selections are retrieved using the ListBoxGetSelectArray function, and we can simply look in the array for “Other”. In the affirmative, we enable the “Other” text field. Otherwise we disable it.
Since control action is closely related to state management, you will notice that there is significantly less code in this section. It simply looks for the control ID and the action code LBN_SELCHANGE. I am not sure why the developers at Microsoft decided this, but the style LBS_NOTIFY must be added in order to receive select notifications. Forgetting to add this style can lead to a great deal of frustration in trying to understand why a click action on a list box is not processed.
There is a functional difference between the checkbox and list box versions. In the checkbox version, checking “None” cleared all the other selections. I did not add that functionality in the control action for the list box. It would be simple; can you figure out how? Instead, the conflict of selecting “None” and other items is tested in validation.
Look at load. It is also significantly less voluminous. We begin by loading the template items in the list box using the ListBoxLoadList function. Rather than coding each item, the allergy string is exploded into an array using the ExplodeString function with a delimiter of “, “. Note that space actually counts as part of the delimiter. If a comma only is used, then the resulting items would have leading spaces. After the explosion, we loop through the list until it’s empty, finding each item in the list box and selecting it.
For validation, retrieving the user’s selection is super easy, again using the ListBoxGetSelectArray function and then the ImplodeArray function with the appropriate glue. As is the case, the remain of the code verifies that the user input is correct. We test for specific conditions: “Other”, to be certain we have the associated description; Empty, because the user must select something; and “None”, because has nothing else should be selected (remember the option of adding this as a state change?).
List Box API
Covering only what we are using (there are over 20 list box API functions), we will start with the ListBoxLoadList function:
int = ListBoxLoadList ( int id, string data );
This is a very flexible function allowing for the data parameter to be delimited with commas, spaces or line endings.
int = ListBoxFindItem ( int id, string target );
The search operation in the ListBoxFindItem function returns the zero based index of the item or -1 on error for the target target item. It finds the first string in the list box that exactly matches the specified string. The search is not case-sensitive.
int = ListBoxSelectItem ( int id, int index, [boolean state] );
List boxes have two select modes: single and multiple. This function is used in multiple mode to set the state of an individual item.
string[] = ListBoxGetSelectArray ( int id );
This last function is very useful in that everything selected in the list box can be captured with one function.
List Box Resources
Our resources are fairly simple:
ListBoxExample01Dlg DIALOGEX 0, 0, 240, 150 EXSTYLE WS_EX_DLGMODALFRAME STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "List Box Example" FONT 8, "MS Shell Dlg" { CONTROL "Food &Allergies:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 6, 6, 60, 8, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 56, 11, 176, 1 CONTROL "", LB_SELECTIONS, "listbox", LBS_NOTIFY | LBS_MULTIPLESEL | LBS_NOINTEGRALHEIGHT | WS_BORDER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 14, 19, 40, 92 CONTROL "Other E&xplain:", LB_OTHER_TITLE, "static", WS_CHILD | WS_VISIBLE, 65, 85, 60, 12, 0 CONTROL "", LB_OTHER_TEXT, "edit", ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 65, 98, 160, 12 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 6, 120, 226, 1 CONTROL "OK", IDOK, "button", BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 127, 126, 50, 14, 0 CONTROL "Cancel", IDCANCEL, "button", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 180, 126, 50, 14, 0 }
We are using LBS_NOTIFY, LBS_MULTIPLESEL and LBS_NOINTEGRALHEIGHT. The last property is not discussed here. No integral height is used to control the button of the list box. By default, list box height will snap to position so as to not cut off display of the that last item in the display area. However, it makes it very difficult to align other controls to the bottom of the box.
A side note on the quick keys. The list box quick key is Alt+A attached to the preceding static title “Food &Allergies:”. It can also be put into the title text of the list box control but will not be visible to the user.
Conclusion
For our example application, I think the checkbox option is the best user interface. That being said, let us compare the two:
Metric | Checkbox | List Box | ||
User Experience | Best | Ok — Better in some environments | ||
Lines of Code | 244 | 131 | ||
Complexity | Straight Forward | Abstract | ||
Extensibility | Poor | Excellent | ||
Ability to ‘tristate’ controls | Yes | No |
I did not cover some of the more sophisticated checkbox options like tri-state and icons. That is for another day. In addition, rather than using a list box, the Data Control class could be used. It allows for coloring, styling, checkboxes in list as well. That too is for another blog.
In the next blog, I will cover radio buttons and the ever-confusing concept of control grouping.
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. |
Additional Resources
Legato Script Developers LinkedIn Group
Primer: An Introduction to Legato