Allowing the user to select items or review lists of data is fundamental to many interfaces. For one or two items, radio buttons or check boxes can work, but for more complex data, a list box can be a good choice. In this blog, I will cover the ‘listbox’ common control, one of many methods of presenting and managing lists of data.
Friday, May 11. 2018
LDC #84: Dialog Boxes Part III — List Boxes
Introduction
The list box is a common control employed by many programs to display, select, and edit information. Windows offers a common control class, ‘listbox’, and Legato layers API functions to load and control information within a list box. Our sample lets us test some of the common API functions. It looks like this:
The list box it itself consists of a list of string items that can be sorted or unsorted and optional scrollbars. Items can also be set in primitive columns using conventional tabs. Scrollbars can be automatic, meaning they appear when the list becomes too large to fit in the allotted space.
Items can be selected singly (only one at a time) or via a multiple select. This example is uses single select mode.
Our test code is as follows:
// // Simple List Box Test // -------------------- // #define LB_TEXT 101 #define LB_LIST 102 #define LB_LOAD 301 #define LB_FOLDER 302 #define LB_RESET 306 #define LB_INSERT 307 #define LB_DELETE 308 #define LB_SELECT 309 #define LB_SEL_RESET 310 #define LB_INDEX 401 #define LB_CONTENT 402 #define LB_TOTAL_ITEMS 403 #beginresource ListBoxTest01 DIALOGEX 0, 0, 250, 155 EXSTYLE WS_EX_DLGMODALFRAME STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION CAPTION "List Box Test #1 (Single Select)" 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 "", LB_TEXT, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 12, 20, 120, 12, 0 CONTROL "", LB_LIST, "listbox", LBS_STANDARD | WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP, 12, 39, 120, 53, 0 CONTROL "Load", LB_LOAD, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 143, 15, 45, 12, 0 CONTROL "Folder", LB_FOLDER, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 143, 30, 45, 12, 0 CONTROL "Reset", LB_RESET, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 195, 15, 45, 12, 0 CONTROL "Insert", LB_INSERT, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 195, 30, 45, 12, 0 CONTROL "Delete", LB_DELETE, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 195, 46, 45, 12, 0 CONTROL "Select", LB_SELECT, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 195, 62, 45, 12, 0 CONTROL "Sel Reset", LB_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 "", LB_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 "", LB_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 "", LB_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 list_update (); /* Our Support for Dialog Status */ /* */ /* ** Static Data */ int s_ix; /* Persistent Select Index */ /************************************************/ /****************************************/ /* Program Entry */ /****************************************/ int main () { DialogBox("ListBoxTest01", "list_"); return ERROR_NONE; } /************************************************/ /****************************************/ /* Dialog Service - Load */ /****************************************/ int list_load() { ListBoxAddItem(LB_LIST, "Item 1 - Cats"); ListBoxAddItem(LB_LIST, "Item 2 - Dogs"); ListBoxAddItem(LB_LIST, "Item 3 - Ferrets"); ListBoxAddItem(LB_LIST, "Item 4 - Mouses"); list_update(); EditSetText(LB_TEXT, "Set Text Test"); return ERROR_NONE; } /****************************************/ /* Dialog Service - Action */ /****************************************/ void list_action(int c_id, int c_ac) { string s1; int rc, width; if (c_id == LB_LIST) { // List Box if (c_ac == LBN_SELCHANGE) { list_update(); return ; } if (c_ac == LBN_DBLCLK) { rc = ListBoxGetSelectIndex(LB_LIST); s1 = FormatString("Double Click on %d", rc); EditSetText(LB_CONTENT, s1); return ; } return ; } if (c_id == LB_LOAD) { // Load s1 = EditGetText(LB_TEXT); if (s1 == "") { s1 = "A1, A2 B, A3 CC, A4 DDD, A5 E E E, A6 END"; } ListBoxReset(LB_LIST); ListBoxLoadList(LB_LIST, s1); list_update(); EditSetText(LB_CONTENT, s1); return ; } if (c_id == LB_FOLDER) { // Load Folder s1 = EditGetText(LB_TEXT); if (s1 == "") { s1 = "C:\\Windows\\*.*"; } ListBoxReset(LB_LIST); ListBoxLoadFolder(LB_LIST, s1, DDL_ARCHIVE); list_update(); EditSetText(LB_CONTENT, s1); return ; } if (c_id == LB_RESET) { // Reset ListBoxReset(LB_LIST); list_update(); return 0; } if (c_id == LB_INSERT) { // Insert s1 = EditGetText(LB_TEXT); if (s1 == "") { s1 = "Default Insert Text"; } rc = ListBoxGetItemCount(LB_LIST); if (rc != 0) { ListBoxInsertItem(LB_LIST, 1, s1); } else { ListBoxAddItem(LB_LIST, s1); } list_update(); return ; } if (c_id == LB_DELETE) { // Delete s_ix = ListBoxGetSelectIndex(LB_LIST); if (s_ix < 0) { MessageBox('X', "Nothing selected"); } ListBoxDeleteItem(LB_LIST, s_ix); list_update(); return ; } if (c_id == LB_SELECT) { // Select ListBoxSetSelectIndex(LB_LIST, s_ix); s_ix++; list_update(); return ; } if (c_id == LB_SEL_RESET) { // Select Reset s_ix = -1; ListBoxSetSelectIndex(LB_LIST, s_ix); list_update(); return ; } } /****************************************/ /* Dialog Support */ /****************************************/ void list_update() { string s1; int ix, ic; ix = ListBoxGetSelectIndex(LB_LIST); ic = ListBoxGetItemCount(LB_LIST); if (ix < 0) { EditSetText(LB_INDEX, "(not selected)"); EditSetText(LB_CONTENT, "(no data)"); ControlDisable(LB_DELETE); } else { EditSetText(LB_INDEX, ix); s1 = ListBoxGetItemText(LB_LIST, ix); EditSetText(LB_CONTENT, s1); ControlEnable(LB_DELETE); } EditSetText(LB_TOTAL_ITEMS, ic); }
In addition to the list box control, we have some other controls on the dialog to interact with it and help explore its capabilities. There’s an edit control through which we can enter a list of options that can be loaded into the list box. In addition, we have a multitude of buttons (“Load”, “Insert”, “Select”, etc) that also perform varied operations with and on the list box.
The List Box Resource
The list box is created using the conventional CONTROL dialog resource statement with the class ‘listbox’.
CONTROL "", LB_LIST, "listbox", LBS_STANDARD | WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP, 12, 39, 120, 53, 0
The opening item is the initial text for the list box; here we leave it blank. Then comes the control ID for which we have created a define (LB_LIST). Then the class name, “listbox”. Next are the styles associated with the control. As always, for most controls we have WS_CHILD, WS_VISIBLE, and WS_TABSTOP specified. We have also specified WS_VSCROLL. Without additional styles, the scrollbar will only appear if the list becomes too large for the size of the control. The blue highlighted item is one of many options to determine the control’s behavior. We’ll cover that more below.
The next items are the control’s conventional x/y position and height/width. By default, a list box’s height is snapped to a complete row, which means that when adjusting the height by a small amount, it may appear as if nothing happens. The height value must exceed the ‘integral height’ before the box will get larger.
Let’s look at some of our available bitwise style options:
Constant | Description | |||
LBS_DISABLENOSCROLL | Shows a disabled horizontal or vertical scrollbar when the list box does not contain enough items to force it to scroll. If you do not specify this style, the scrollbar is hidden when the list box does not contain enough items. This style must be used with the WS_VSCROLL or WS_HSCROLL style. | |||
LBS_EXTENDEDSEL | Allows multiple items to be selected by using the SHIFT key and the mouse or special key combinations. | |||
LBS_MULTIPLESEL | Turns string selection on or off each time the user clicks or double-clicks a string in the list box. The user can select any number of strings. | |||
LBS_NOINTEGRALHEIGHT | Specifies that the size of the list box as exactly the size specified by the application when it created the list box. Normally, the system sizes a list box so that the list box does not display partial items. | |||
LBS_NOSEL | Specifies that the list box contains items that can be viewed but not selected. | |||
LBS_NOTIFY | Causes the list box to send a notification code to the parent window whenever the user clicks a list box item (LBN_SELCHANGE), double-clicks an item (LBN_DBLCLK), or cancels the selection (LBN_SELCANCEL). | |||
LBS_SORT | Sorts the strings in the list box alphabetically. | |||
LBS_STANDARD | Sorts the strings in the list box alphabetically. The parent window receives a notification code whenever the user clicks a list box item, double-clicks an item, or cancels the selection. The list box has a vertical scrollbar and borders on all sides. This style combines the LBS_NOTIFY, LBS_SORT, WS_VSCROLL, and WS_BORDER styles. | |||
LBS_USETABSTOPS | Enables a list box to recognize and expand tab characters when drawing its strings. Use the ListBoxSetTabPositions function to set the tab stops in dialog units. |
Our choice for our list box was LBS_STANDARD which combines certain notification, sorting, and scrolling parameters as shown above.
Note that some controls have multiple levels of notification which comes through the action procedure. In this case, if we want to know that the user made a change to the selection, we need LBS_NOTIFY turned on.
Loading Data
There are multiple methods of loading data into a list box. Obviously one can add or insert items one by one:
int = ListBoxAddItem ( int id, mixed value );
or
int = ListBoxInsertItem ( 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 box. 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.
For string values, a tab delimiter (‘/t’ or 0x09) will cause the data to be split at the tab stops if specified.
Both the ListBoxAddItem and ListBoxInsertItem 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 ListBoxLoadList function works well:
int = ListBoxLoadList ( int id, string data );
The data parameter string can be in one of three forms: lines, comma separated, or space separated. 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 control is empty, the list box will be populated with default data.
An array or list of data can be added using the ListBoxAddArray function:
int = ListBoxAddArray ( int id, string array );
Each array item is added to the list. If LBS_SORT is enabled, the resulting list is sorted.
Finally, data such as a file in a folder can be added using the ListBoxLoadFolder 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 folder’s contents. Leaving the edit control empty loads the Windows folder.
Reading Data
Items can also be read from a list box. This is useful if one elects to use the list box as a sort of edited data repository during the dialog session. For example, you can allow the user to add, delete or swap items, and then you can retrieve the altered list. Items can be read as an array using the ListBoxGetArray function:
string[] = ListBoxGetArray ( int id );
If you need to get a single item:
string = ListBoxGetItemText ( int id, int index );
If you also want to know how many items are in the list box:
int = ListBoxGetItemCount ( int id );
Check the result for an error using the IsError function before assuming the value is the count. In our example, the ListBoxGetItemCount function is used both for displaying status and for the Insert button.
A move up or move down function can be created by using the ListBoxGetItemText and ListBoxReplaceItem functions. For example, try replacing the select reset button, Sel Reset, with the following moving up code:
string s2; int ix; ix = ListBoxGetSelectIndex(LB_LIST); if (ix > 0) { s1 = ListBoxGetItemText(LB_LIST, ix); s2 = ListBoxGetItemText(LB_LIST, ix - 1); ListBoxReplaceItem(LB_LIST, ix - 1, s1); ListBoxReplaceItem(LB_LIST, ix, s2); ListBoxSetSelectIndex(LB_LIST, ix - 1); }
Note that we had to add two new variables to add this code. Our new code checks to make sure something is selected and that the selection is not the first item (obviously, item 0 cannot be moved up). The swap is performed and then we reselect the next item. This allows the user to push ‘move up’ in succession without reselecting. Similar logic can be added to delete operations such that when the last item is deleted, the selection is moved up. To make this more user friendly, the action procedure can also enable or disable the move up/down buttons based on whether there is a selection and whether the selection is the first or last item in the list.
Using Tab Stops
The listbox class has a simple column feature using tab stops. To enable the feature, the LBS_USETABSTOPS style bit must be added to the CONTROL statement. Positions are set using the ListBoxSetTabPositions function:
int = ListBoxSetTabPositions ( int id, [int p1, int p2, ...] | [int list[] ] );
Tab positions can be listed one after another as parameters or as an integer array. They need to be in ascending order and are expressed in dialog units. Dialog units insure that the positions will still be acceptable if the theme changes.
Adding tabs to any inserted string will cause the display position to snap to the next stop. All positions are left aligned on the tab stop position. Use caution with wildly varying string sizes since a long segment may cause the tab to snap to the next position should the text width exceed the space between tab stops.
Removing Items
If you want to clear a list completely, the ListBoxReset function will remove every item in the list. This is useful when the list needs to be reloaded. Pressing the Reset button demonstrates the list box reset.
If a single it item is to be deleted, the ListBoxDeleteItem 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 select and even move it if the last item was deleted.
Finally, as stated above, the ListBoxReplaceItem function allows an entry to be replaced.
int = ListBoxReplaceItem ( int id, int index, string data );
Replacing an item does not change the sorting of the list box.
Selecting and Deselecting
List boxes operate is three different select modes: no select, single select, and multiple select. The default and most common mode is single select. By not altering any style flags, the list box will be in single row select mode as shown in our example.
In no select mode (LBS_NOSEL), clicking or using the keyboard arrows will not visibly select any items in the list box. However, the list box will still process selections and remember select positions as set through the API.
In single select mode, one item can be selected using the following function.
int = ListBoxSetSelectIndex ( int id, int index );
The index is zero based. To reset or remove the selection, set the index to -1. A more common action is to read the selection, and for this we use the ListBoxGetSelectIndex function:
int = ListBoxGetSelectIndex ( int id );
If nothing 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)) { ... }
For multiple select list boxes, the LBS_MULTIPLESEL must be used. This changes the behavior of the list box such that the user can use the control and shift keys to select more than one item. Note that the above select routines do not work in this mode. Rather, a series of multiple select functions are provided. The first function allows items to be individually selected:
int = ListBoxSelectItem ( int id, int index, [boolean state] );
Simply specify the index and the state. The default state is TRUE. Selections can be read one by one or as an array. First, we can get a count:
int = ListBoxGetSelectCount ( int id );
This is fairly straightforward except you should note that the return value can be a formatted error code. For example, if the function is used outside of a dialog or on an invalid control, it would return an error. The selected items can be returned as an array of zero-based indices or as an array containing the text of the selected items:
int[] = ListBoxGetSelectList ( int id );
string[] = ListBoxGetSelectArray ( int id );
If nothing is selected, the result will contain zero elements. Finally, the counterpart to retrieving select states as an array is to test a single item:
int = ListBoxTestItemSelect ( int id, int index );
The return value can be FALSE, TRUE, or a formatted error code. As such, it is better to check for TRUE to avoid a non-zero error code being interpreted as positive condition.
Notifications
The listbox class sends the following notification codes via the action procedure:
Notification code | Value | Description | ||||
---|---|---|---|---|---|---|
LBN_DBLCLK | 2 | The user double-clicks an item in the list box. | ||||
LBN_ERRSPACE | -2 | The list box cannot allocate enough memory to fulfill a request. | ||||
LBN_KILLFOCUS | 5 | The list box loses the keyboard focus. | ||||
LBN_SELCANCEL | 3 | The user cancels the selection of an item in the list box. | ||||
LBN_SELCHANGE | 1 | The selection in a list box is about to change. | ||||
LBN_SETFOCUS | 4 | The list box receives the keyboard focus. |
The most used items are LBN_SELCHANGE and LBN_DBLCLK. Select change can be used to update data within the dialog. Clicking on “Ferrets”:
Within the action procedures, we see the following code:
if (c_id == LB_LIST) { // List Box if (c_ac == LBN_SELCHANGE) { list_update(); return ; } if (c_ac == LBN_DBLCLK) { rc = ListBoxGetSelectIndex(LB_LIST); s1 = FormatString("Double Click on %d", rc); EditSetText(LB_CONTENT, s1); return ; } return ; }
The ID is first checked for the list box, then each of two action notification sub messages are checked and processed.
Enabling or disabling controls related to the list box creates a more refined dialog interface.
Another action that is exceptionally useful (and I think shows a bit of refined programming) is the double-click action. For a list box, it not only selects but also is the same as pressing OK. This is very easy to add by just detecting the double click and running the DialogPostOK function.
Conclusion
List boxes have a particular place for dealing with user data, input, and selections. While the common control has a good feature set, sometimes more options are needed. For example, the tab feature in the list box class is weak because it cannot be easily controlled and it does not constrain column data. Other classes, such a Data Control, have a lot more feature functionality and operate on the same basis.
Another control used frequently is the combo box, which, as named, is a combination of a text edit control and a list box. Having a firm understanding of list boxes provides a good foundation to discuss combo boxes. That will be covered in my next article. Until then, have fun!
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