DesktopListBox

From Xojo Documentation

Class (inherits from DesktopUIControl)


New in 2021r3

The scrollable DesktopListBox control is used to display one or more columns of information. You can add a checkbox and/or a picture to a row and make a cell or column editable. You can control font style on a cell-by-cell basis and set the column alignment. With some programming, you can create hierarchical lists that use disclosure triangles to show nested items.

Events
CellAction DragOverRow MouseExit
CellFocusLost DragReorderRows MouseMove
CellFocusReceived DragRow MouseUp
CellKeyDown DropObject MouseWheel
CellPressed DropObjectOnRow Opening
CellTextChanged FocusLost PaintCellBackground
Closing FocusReceived PaintCellText
ColumnSorted HeaderPressed PaintDisclosureWidget
ConstructContextualMenu KeyDown PaintHeaderBackground
ContextualMenuItemSelected KeyUp PaintHeaderContent
DoublePressed MenuBarSelected RowCollapsed
DragEnter MouseDown RowComparison
DragExit MouseDrag RowExpanded
DragOver MouseEnter SelectionChanged
Properties
Active fa-lock-32.png HasBorder Parent
ActiveTextControl fa-lock-32.png HasHeader RequiresSelection
AllowAutoDeactivate HasHorizontalScrollbar RowCount fa-lock-32.png
AllowAutoHideScrollbars HasVerticalScrollbar RowHeight fa-lock-32.png
AllowExpandableRows HeaderHeight RowSelectionType
AllowFocusRing HeadingIndex Scope fa-lock-32.png
AllowResizableColumns Height ScrollPosition
AllowRowDragging Index fa-lock-32.png ScrollPositionX
AllowRowReordering InitialValue SelectedRowCount fa-lock-32.png
AllowTabStop Italic SelectedRowIndex
Bold LastAddedRowIndex fa-lock-32.png SelectedRowValue
ColumnCount LastColumnIndex fa-lock-32.png SortingColumn
ColumnWidths LastRowIndex fa-lock-32.png TabIndex
DefaultRowHeight Left Tooltip
DropIndicatorVisible LockBottom Top
Enabled LockLeft Transparent
FontName LockRight Underline
FontSize LockTop Visible
FontUnit MouseCursor Width
GridLines Name fa-lock-32.png Window fa-lock-32.png
Handle fa-lock-32.png PanelIndex
Methods
AcceptFileDrop CellTextAt PressHeader
AcceptPictureDrop CellToolTipAt Refresh
AcceptRawDataDrop CellTypeAt RefreshCell
AcceptTextDrop CellUnderline RemoveAllRows
AddExpandableRow Close RemoveRowAt
AddExpandableRowAt ColumnAlignmentAt RowAt
AddRow ColumnAlignmentOffsetAt RowDepthAt
AddRowAt ColumnAttributesAt RowExpandableAt
AddRows ColumnFromXY RowExpandedAt
CellAlignmentAt ColumnSortDirectionAt RowFromXY
CellAlignmentOffsetAt ColumnSortTypeAt RowImageAt
CellBold ColumnTagAt RowSelectedAt
CellBorderColor ColumnTypeAt RowTagAt
CellCheckBoxStateAt Content Rows
CellCheckBoxValueAt DrawInto SetFocus
CellItalic EditCellAt Sort
CellTagAt HeaderAt
Enumerations
Alignments GridLineStyles SortTypes
CellTypes RowSelectionTypes
DropLocations SortDirections

Class Constants

The following class constants of the DragItem class can be used with HeaderAt to specify that a method should apply to all columns or rows rather than a specific column or row.

Class Constant
AllColumns
AllRows
NoSelection

Notes

Items in a single-column DesktopListBox can be accessed using the CellValueAt method as the column parameter defaults to 0. The following example gets the value in the first row of a DesktopListBox:

Var lastName As String
lastName = ListBox1.CellValueAt(0)

Iterating Through Rows

The Rows method returns a DesktopListBoxRow which allows you to easily iterate through rows. In this example, the Tag of each row is examined and if it's found to be "Taxable", the ComputeTaxes method is called and passed the value of the row.

For Each row As DesktopListboxRow In Listbox1.Rows
If row.Tag = "Taxable" Then ComputeTaxes(row.Value)
Next

Multi-Column DesktopListBoxes

You can create multi-column DesktopListBoxes by changing the ColumnCount property. Because the first column in a multi-column DesktopListBox is column 0 (zero), the ColumnCount property will always be one more than the number of the last column. The maximum number of columns is 256 (columns 0 through 255). You should set ColumnCount to the number of columns that you want to display. If you want to put data in an invisible column, set the column width to zero.

You can use the InitialValue property to set up the inital values of multi-column ListBoxes by separating the column values with tabs and row values with carriage returns.

The widths of columns in multi-column DesktopListBoxes can be set by passing the widths as a list of values separated by commas to the ColumnWidths property. The widths can be passed in points or as percentages of the total width of the DesktopListBox. If you don't pass widths for all the columns, the remaining columns will be evenly spaced over the remaining space. If too many widths are passed, the additional values are ignored. If the total of the widths passed is greater than the width of the DesktopListBox, then the remaining columns will be truncated.

Specific cells in a multi-column DesktopListBox can be accessed using the CellValueAt method. To populate a multi-column DesktopListBox, first use the AddRow method to create the new row and populate the first column. Then use the CellValueAt method to add values to the other columns in the row. Use the LastAddedRowIndex property to get the index of the row you just added with AddRow.

For example, the following code populates a two-column DesktopListBox with the names of the controls in the window and their indexes.

For i As Integer = 0 To Self.ControlCount - 1 // number of controls in window
ListBox1.AddRow(Str(i)) // first column
ListBox1.CellValueAt(Listbox1.LastAddedRowIndex, 1) = DesktopUIControl(i).Name // second column
Next

Determining which cell was double-clicked

The DoublePressed event fires when the user double-clicks anywhere inside a DesktopListBox, but the indexes of the cell that was double-clicked are not passed. You can determine which cell was double-clicked with the RowFromXY and ColumnFromXY methods. They use the x,y mouse coordinates where the double-click took place and translate them into the row and column indexes of the cell that was clicked. You need to adjust for the location of the ListBox on the screen relative to the top-left corner of the display.

This code in the MouseDown event obtains the indexes of the cell that was double-clicked.

Var row, column As Integer
row = Me.RowFromXY(x, y)
column = Me.ColumnFromXY(x, y)
MessageBox("You double-clicked in cell " + row.ToString + ", " + column.ToString)

The parameters of RowFRomXY are relative to the top, left corner of the DesktopListBox on a window. If you use the DesktopListBox in a DesktopContainer you have to take into account the distance of the container from the window edges.

Var row, column As Integer
row = Me.RowFromXY(System.MouseX - Me.Left - Self.Left - Me.Window.Left, System.MouseY - Me.Top - Self.Top - Me.Window.Top)
column = Me.ColumnFromXY(System.MouseX - Me.Left - Me.Parent.Left - Me.Window.Left, System.MouseY - Me.Top - Me.Parent.Top - Me.Window.Top)
MessageBox("You double-clicked in cell " + row.ToString + ", " + column.ToString)

Making a Cell Editable

Use the CellTypeAt or ColumnTypeAt properties to change a cell or column to "inline editable" when you want the user to be able to edit the contents of the DesktopListBox. Then call the EditCellAt method for each cell. This gives the focus to the editable cell and selects the current text of the cell, if any. Typing replaces the cell's contents. When the user presses Tab or Return or clicks in another cell, the cell loses the focus and the contents of the cell are saved.

The following code in the CellPressed event makes the cell the user pressed on editable. The parameters row, and column are passed to the function.

Me.CellTypeAt(row, column) = ListBox.CellTypes.TextField
Me.EditCellAt(row, column)

When a cell is editable, the ActiveTextControl property is the DesktopTextField that contains the contents of that cell. You can use this property to set or get the text of the DesktopListbox cell, set the selection, or change other properties of the DesktopListBox's DesktopTextField.

Decimal Alignment

When you use decimal alignment in a cell or column, you must take into account the fact that the decimal separator is aligned with the right edge of the column or cell. You must pass a negative number to CellAlignmentOffsetAt or ColumnAlignmentOffsetAt to make room for the numbers to the right of the decimal place. The correct value to pass depends on the number of digits to the right of the decimal place in the column or cell.

Hierarchical ListBoxes

Creating a simple hierarchical DesktopListBox is more involved than a two-column DesktopListBox because you must manage hiding and displaying the sublist data. A simple way to do this is to assign the sublists to a "hidden" column in the DesktopListBox and toggle the display of that data when the user double-clicks on a "parent" element.

You create a row with a disclosure triangle using the AddExpandableRow method (rather than the AddRow method) and then set the AllowExpandableRows property to True. See the Example for a simple hierarchical DesktopListBox with one level.

Resizing Columns

There are two "modes" for column resizing. There is no formal mode property. Rather, the "mode" is implicitly set according to whether every column width is specified as an absolute amount. If you specify all columns either in points or as a percentage, you will be using the second mode. If you use an asterisk or leave a column width blank, you will be using the first mode.

  • A change to one column width affects the width of another column.

If column i gets bigger, column i+1 gets smaller by the same amount. This mode is great when using a ListBox without a horizontal scrollbar. You turn this mode on when you have at least one column width that is blank, or specified using an asterisk (e.g. "", " ", "*", or "4*").

Note: By design you can't resize the right edge of the last column in this mode. To resize the last column you need to resize the previous column.

  • Each column width is independent and can grow or shrink on its own.

You are responsible when the user does this, and you need to provide a horizontal scrollbar so that the user can get to the any headers that have been pushed out of view to the right. You enable this mode by making sure every column width is specified in terms of an absolute pixel width, or a percentage width (e.g. "20", or "35%"). If you use an asterisk or leave a column width blank, you will automatically be using the first mode.

You can switch between mode 1 and 2 at runtime using the same criteria as above.

The ColumnWidths property is equivalent to the concatenation of all of the ColumnWidthExpressions.

ColumnWidthExpressions are strings and they can represent several different types of column width calculations: absolute points (e.g., "45"), percentages (e.g. "22.3%"), and asterisk widths (or blanks), e.g. " ", "4*". The value "*" is equivalent to "1*" and can be used to mean "fill the remaining space."

ColumnWidthExpressions retain their type even when a column is resized. This means that if you:

  • Resize a window to which a ListBox is locked, it will grow or shrink. The columns grow or shrink as well if their expressions were *-based (unless you use "0*"), or percentage based (0%). If you want them to stay fixed, you need to express the ColumnWidthExpression as an absolute pixel value.
  • Resize a column by dragging it internally, it will recompute its percentage or asterisk value. This is so that you can, say, start with a two-column DesktopListBox with no column widths specified (each column will take up half the space). Then drag one column to take up 3/4 of the space, then enlarge the DesktopListBox, and now both column widths will enlarge so that their widths remain in a 3/4 to 1/4 ratio.

Changing the pixel value of a column will not change its fundamental type, but will change the value of that type.

Finally, if you want to create columns that won't get resized, change the UserResizable property for each of the columns in question. If you are using mode 1, you will need to change the UserResizable property for both the column and the one to its left.

Databases

A DesktopListBox is often used to display the results of database queries. A DesktopListBox can be populated with the results of a query programmatically. See the example "Database Example" in the Examples folder.

CheckBox Cells

The CellCheckBoxStateAt method enables you to get or set the value of a tri-state Checkbox cell. Any cell of type Checkbox can store one of three values: Checked, Unchecked, and Indeterminate.

To set up a cell as a Checkbox, use code such as this in the Opening event:

Me.CellTypeAt(1, 0) = DesktopListBox.CellTypes.CheckBox

To change the state of the cell, use the VisualStates enumeration of the CheckBox control:

ListBox1.CellCheckBoxStateAt(1, 0) = DesktopCheckbox.VisualStates.Indeterminate

The Indeterminate state places a minus sign in the checkbox (macOS) or fills in checkbox (Windows and Linux).

Customized Scroll Controls

Suppose you want a horizontal scroll bar that leaves room for a pop-up menu. In this example, a DesktopScrollbar control has been added to the bottom area of the ListBox and a DesktopBevelButton control has been added to its right. The two controls take up the area that would be used by the built-in horizontal scrollbar.

The DesktopScrollbar control has the following code in its Opening event handler:

Me.Maximum = 50
Me.Minimum = 0
Me.LineStep = 5

The values for Maximum and LineStep were chosen to match the total width of the DesktopListBox's columns. Adjust these values to suit your DesktopListBox. Its ValueChanged event handler has the following line of code:

ListBox1.ScrollPositionX = Me.Value

In this way, the user can scroll the ListBox horizontally, bringing all columns into view.

The DesktopBevelButton enables the user to switch the DesktopListBox between single-line selection and multiple-line selection. The DesktopBevelButton is set to have a normal menu and its Open event handler populates the menu with two items:

Me.AddRow("Single-line")
Me.AddRow("Multiple-line")

The BevelButton's Action event sets the value of the DesktopListBox's SelectionType property:

Select Case Me.MenuValue
Case 0
Listbox1.RowSelectionType = DesktopListBox.RowSelectionTypes.Single
Case 1
ListBox1.RowSelectionType = DesktopListBox.RowSelectionTypes.Multiple
End Select

Horizontal and Vertical Grid Lines

You can show horizontal and vertical rules for the entire ListBox using the GridLines property. The Default value is equivalent to "None". Other choices are: Horizontal, Vertical, or Both.

Sample Code

Adding a row to ListBox1:

ListBox1.AddRow("October")

Adding a row at row 1 in ListBox1:

ListBox1.AddRowAt(1, "October")

Creating a three-column ListBox and adding the headings and the first row of information:

ListBox1.ColumnCount = 3

ListBox1.HasHeader = True
ListBox1.HeaderAt(0) = "Name"
ListBox1.HeaderAt(1) = "Phone"
ListBox1.HeaderAt(2) = "Email"

ListBox1.AddRow("Milton")
ListBox1.CellValueAt(ListBox1.LastAddedRowIndex, 1) = "555-2212"
ListBox1.CellValueAt(ListBox1.LastAddedRowIndex, 2) = "milt@fredonia.com"

Changing all items in the ListBox to bold, underline:

ListBox1.Bold = True
ListBox1.Underline = True

Copying the fifth element of ListBox1 to another variable:

Var e As String
e = ListBox1.List(4)

Adding a column to ListBox1 and setting the widths of the columns to 50 and 65 points, respectively:

ListBox1.ColumnCount = 2
ListBox1.ColumnWidths = "50,65"

Setting the number of columns of ListBox1 to three and setting the widths of the columns to 60%, 20% and 20% respectively:

ListBox1.ColumnCount = 3
ListBox1.ColumnWidths = "60%,20%,20%"

If ListBox1 is 100 points wide and has three columns, the following code will set the columns widths as indicated but the last column will only be 10 points wide instead of 20:

ListBox1.ColumnWidths = "60,30,20"

If ListBox1 is 100 points wide and has three columns, the following code will set the columns widths but the last column will not be displayed:

ListBox1.ColumnWidths = "60,40,20"

Copying the fifth row of the third column of ListBox1 to another variable:

Var e As String
e = ListBox1.CellValueAt(4, 2)

Assigning a value to the fifth row of the third column of ListBox1:

ListBox1.CellValueAt(4, 2) = "Bill"

Setting the fifth row of the third column of ListBox1 to bold, italic:

ListBox1.CellBold(4, 2) = True
ListBox1.CellItalic(4, 2) = True

Adding a row with the text "Users" in the first cell and placing an image of a folder to the left of the text. The picture "usersFolder" has been added to the project.

ListBox1.AddRow("Users")
ListBox1.RowImageAt(0) = UsersFolder

Setting up the DragRow event handler to allow the user to drag a value from a ListBox:

Function DragRow(Drag As DragItem, Row As Integer) As Boolean
Drag.Text = ListBox1.List(Row)
Return True
End Function

Summing the numeric values of the selected rows:

Var total As Integer
For i As Integer = 0 To ListBox1.LastRowIndex
If ListBox1.Selected(i) Then
total = total + Val(ListBox1.List(i))
End If
Next

This code expands the first row of ListBox1 (if it is collapsed) or collapses it (if it was expanded). The row must have been added with the AddFolder method:

ListBox1.ExpandedRowAt(1) = Not ListBox1.ExpandedRowAt(1)

This code populates a three-column ListBox with headings:

ListBox1.HasHeader = True
ListBox1.HeaderAt(0) = "ID"
ListBox1.HeaderAt(1) = "JobTitle"
ListBox1.HeaderAt(2) = "Name"

This code sets up a ListBox with four visible columns plus one hidden column. Column zero is hidden:

Me.ColumnCount = 5
Me.ColumnWidths = "0,25%,25%,25%,25%"
Me.HasHeader = True
Me.HeaderAt(0) = "ID"
Me.HeaderAt(1) = "FirstName"
Me.HeaderAt(2) = "LastName"
Me.HeaderAt(3) = "Phone"
Me.HeaderAt(4) = "Zip"

The following line of code displays the value of the hidden column in the selected row:

MessageBox(ListBox1.CellValueAt(ListBox1.SelectedRowIndex, 0))

Hierarchical ListBoxes

A hierarchical ListBox works like a tree view. Xojo uses the generic terms Expandable Rows to describe this style of ListBox. To allow for expandable rows, set the AllowExpandableRows property to true then rows can be expanded to display child content (and then collapsed later).

Windows uses plus and minus signs to indicate rows that are parents; macOS and Linux use disclosure triangles.

The following code, which is in the ListBox's Open event handler, populates a ListBox with expandable rows: The s1 string contains the parent level and sub1 contains the elements that are nested within each of s1's elements. It is a list of comma-delimited lists, with each list delimited by semicolons. The elements of sub1 are initially hidden because they are stored in a hidden column.

Var u As Integer
Var s1, sub1 As String
Me.ColumnWidths = "150,0"
s1 = "Michigan,Ohio,Minnesota"
sub1 = "Grand Blanc,Bad Axe,Flint,Benton Harbor,Detroit;Cleveland,Columbus,Akron,Pleasantville;St. Paul,Frostbite Falls"
u = CountFields(s1, ",")
For i As Integer =1 To u
If NthField(sub1, ";", i) <> "" Then
Me.AddExpandableRow("")
Me.CellValueAt(i - 1, 1) = NthField(sub1, ";", i)
End If
Me.CellValueAt(i - 1, 0) = NthField(s1, ",", i)
Next
Me.ColumnCount = 1

Note that the AddExpandableRow method, rather than AddRow, is used to add the State names.

The following line of code in the DoubleClick event handler toggles the expanded state of the row that was double-clicked:

Me.RowExpandedAt(Me.SelectedRowIndex) = Not Me.RowExpandedRowAt(Me.SelectedRowIndex)

The following code in the ExpandRow event handler runs when the user double-clicks a collapsed element:

Var s1 As String
Var u As Integer
s1 = Me.CellValueAt(row, 1)
u = CountFields(s1, ",")
For i As Integer = 1 To u
Me.AddRow("")
Me.CellValueAt(Me.LastAddedRowIndex, 0) = NthField(s1, ",", i)
Next

It creates the sublist rows each time the user double-clicks a collapsed state name.

If the ListBox has the AllowExpandableRows property selected, then collapsing is handled automatically when the user collapses an item. If AllowExpandableRows is false, then you need code such as this in the CollapseRow event handler:

Var u, numSubRows As Integer
numSubRows = CountFields(Me.Cell(row, 1), ",")
u = row + 1
For i As Integer = row + numSubRows DownTo u
Me.RemoveRowAt(i)
Next

It removes the rows that were created by the ExpandRow event handler.

To learn more about ListBoxes with expandable rows (also know as "hierarchical ListBoxes"), refer to Hierarhical List Boxes in the User Guide.

Drag and Drop Between ListBoxes

The following example allows the user to drag one row from ListBox1 to ListBox2. ListBox1 has its AllowRowDragging property set to True and its RowSelectionType property set to Single. Its DragRow event handler is as follows:

Function DragRow (drag As DragItem, row As Integer) As Boolean
drag.Text = Me.List(row)
Return True // allow the drag
End Function

ListBox2's Open event handler has the line:

Me.AcceptTextDrop

Its DropObject event handler is this:

Sub DropObject(obj As DragItem)
Me.AddRow(obj.Text) // adds the dropped text as a new row
End Sub

Drag and Drop Multiple Rows Between ListBoxes

The following code allows the user to drag more than one row from ListBox1 to ListBox2. The dragged rows are added to the end of the list.

ListBox1 has its AllowRowDragging property set to True, enabling items in its list to be dragged, and its RowSelectionType property set to Multiple. Its DragRow event handler is as follows:

Function DragRow (Drag As DragItem, Row As Integer) As Boolean
Var nRows As Integer
nRows = Me.ListCount - 1
For i As Integer = 0 To nRows
If Me.Selected(i) Then
Drag.AddItem(0, 0, 20, 4)
Drag.Text = Me.List(i) // get text
End If
Next
Return True // allow the drag
End Function

It uses the AddItem method of the DragItem to add an additional item to the DragItem each selected row. The DropObject event handler then cycles through all items to retrieve all dragged rows.

ListBox2 has the following line of code in its Open event handler. It permits it to receive dragged text.

Me.AcceptTextDrop

Its DropObject event handler checks to see if the dragged object is text; if it is, it adds a row to the end of the list and assigns the text property of the dragged object to the new row: It loops through all items in the DragItem until NextItem returns False.

Sub DropObject(obj As DragItem)
Do
If obj.TextAvailable Then
Me.AddRow(obj.Text)
End If
Loop Until Not obj.NextItem
End Sub

You can also drag from ListBox1 to the desktop to get a text clipping or to another application that supports text drag and drop.

Colors

This code, which is placed in the CellBackgroundPaint event, assigns alternating colors to the rows in a ListBox:

If row Mod 2 = 0 Then
g.DrawingColor= &cD2FFF3
Else
g.DrawingColor= &cD2EDF5
End If
g.FillRectangle(0, 0, g.Width, g.Height)

Notes: The CellBackgroundPaint event passes the parameters g (Graphics), and the row and column numbers (as Integer). You can assign a color to the DrawingColor property by creating it as a constant in the App class or a module and assign the color constant to the DrawingColor property. The following line in the CellTextPaint event draws the text in the preceding example in red:

g.DrawingColor = RGB(255, 0, 0)

The CellTextPaint event is passed the coordinates of the suggested starting position to draw text in the parameters x and y. You can use them in a call to the DrawText method to specify the string to draw in a particular cell:

If row = 4 And column = 1 Then
g.DrawingColor = RGB(255, 0, 0)
g.DrawText("Payment Overdue!", x, y)
End If
Return True

Sorting

To sort a ListBox, set the column on which the ListBox will be sorted with the SortingColumn property. Specify the sort direction on that column with the ColumnSortDirectionAt property, and then do the sort by calling the Sort method.

The following code sorts a Listbox in descending order on the first column.

// first column, descending order
ListBox1.ColumnsortDirectionAt(0) = ListBox.SortDirections.Descending
ListBox1.SortingColumn = 0 // first column is the sort column
ListBox1.Sort

You can also sort a column based on the current value of ColumnSortDirectionAt by calling the PressHeader method. This method programmatically clicks the header for the column passed to it.

Note that sorting is based on string comparisons. If you want to sort numbers or CheckBoxes then you have to use a custom sort.

Custom Sorting

Use the CompareRows event handler to perform custom sorting on the displayed data. You will want to use custom sorting to property sort numerical data, which by default sorts as a string. This causes "2" to be greater than "100" because the values are treated as strings. You can also provide a custom sort for CheckBox columns, dates and any other information that you may want to sort differently than how it displays as a string.

The following example uses the CompareRows event to sort columns of numbers numerically:

Function CompareRows(row1 As Integer, row2 As Integer, column As Integer, ByRef result As Integer) As Boolean
If Val(Me.CellValueAt(row1, column)) > Val(Me.CellValueAt(row2, column)) Then
result = 1
Else
result = -1
End If

Return True // Use the custom sort
End Function

With this code in place, the correct (numerical) sorting is done whenever the user clicks the header area. Test to be sure that the custom sort affects only the numerical columns.

To sort dates, store the SecondsFrom1970 (or SQLiteDate) property of the DateTime in the CellTagAt for the column and use it to sort instead of the displayed value.

See Also

TextField controls; DatabaseField, ListColumn, RowSet classes