Programming Combo Box and List Box Controls in Microsoft Access, Part 2
David Shank
Microsoft Corporation
April 6, 2000
In last month's column, I talked about using the built-in settings of the RowSourceType property of a combo box or list box to specify the source of data for the control. This is a powerful and easy-to-implement solution for displaying data from your database -- but what if you want to display data about the database (a list of table names, for example), or data stored in an array, or data stored outside the database? You can set the RowSourceType property to the name of a custom function that provides any data you want to display in the control.
This month, I'll talk about how to create a custom function to fill a combo box or list box control with any data you want to display. Microsoft® Access calls the function several times when the control is initialized -- but the only thing you have to worry about is correctly creating the custom function so it knows how to respond when Access calls it.
Understanding the Basics
You can't just set the RowSourceType property to the name of any custom function you have created. Because of the special role such a function plays, and because Access calls the function repeatedly as the control is initialized, you must be sure that function adheres to a very specific structure. The function must accept five arguments. The first argument must be declared as type Control, and the remaining arguments must be declared as type Variant. The syntax of the function looks like this:
Function functionname**(**control As Control, id As Variant, row As Variant, column As Variant, code As Variant) As Variant
What Do the Function Arguments Mean?
You can name these arguments anything you want, but they are designed to provide the following information:
Argument | Description |
---|---|
control | A Control object variable that is the combo box or list box control being filled |
id | A unique value that identifies the control being filled |
row | The row being filled (zero-based) |
column | The column being filled (zero-based) |
code | An intrinsic constant representing the information your function should provide |
Because Access calls your function, it is Access that will ask for the information it needs, when it needs it. All you have to do is provide the answers to the questions Access will ask. The code argument described above is the means by which Access will ask its questions. You can determine the question being asked by testing for the intrinsic constant representing the information Access requires. You can think of the intrinsic constant as the question Access asks of your function, and for which your function must provide an answer.
What Do the Intrinsic Constants Mean?
The following table lists the intrinsic constants and the return values that your function should supply:
Constant | Meaning | Function returns |
---|---|---|
acLBInitialize | Initialize | Nonzero if the function can fill the list; False or Null otherwise |
acLBOpen | Open | Nonzero ID value if the function can fill the list; False or Null otherwise |
acLBGetRowCount | Number of rows | Number of rows in the list (can be zero); –1 if unknown |
acLBGetColumnCount | Number of columns | Number of columns in the list (can't be zero); must match the property sheet value |
acLBGetColumnWidth | Column width | Width (in twips) of the column specified by the column argument; –1 to use the default width |
acLBGetValue | List entry | List entry to be displayed in the row and column specified by the row and column arguments |
acLBGetFormat | Format string | Format string to be used to format the list entry displayed in the row and column specified by the row and column arguments; –1 to use the default format |
acLBEnd | End (the last call to the function) | Nothing |
acLBClose | Not used | Not used |
Notice that whenever a particular value (such as the number of columns) is required, returning Null or any invalid value causes Access to stop calling the user-defined function with that code. This also occurs if you return Null when acLBInitialize is called. In addition, when acLBGetValue or acLBGetFormat are requested, you can use the row and column arguments to identify specific values. This means that you must be able to access your data (by row and column) at any time. Since the function is called repeatedly, you can easily preserve the data for the control in a Static array variable that preserves its values between calls to the function.
What Does the Function Look Like?
Because the custom function has to provide specific answers to the questions Access asks about the data, the custom function should be written using the following template:
Function FunctionName(control As Control, id As Variant, _
row As Variant, column As Variant, _
code As Variant) As Variant
Static intRows As Integer
Static intColumns As Integer
Static varDisplayData() As Variant
Dim varRetVal As Variant
Select Case code
Case acLBInitialize
' Any necessary initialization code goes here.
' For example: determine number or rows and number
' of columns, save these values in the intRows and
' intColumns variables, and re-dimension the Static array
' varDisplayData().
varRetVal = True
Case acLBOpen
' Return a unique id value here.
varRetVal = Timer
Case acLBGetRowCount
' Return the number of rows here.
varRetVal = intRows
Case acLBGetColumnCount
' Return the number of columns here.
varRetVal = intColumns
Case acLBGetColumnWidth
' Return the column widths here.
varRetVal = -1
Case acLBGetValue
' Return data to be displayed here.
varRetVal = varDisplayData(row, column)
Case acLBGetFormat
' Return formatting information here.
Case acLBEnd
' Perform any necessary clean up here.
End Select
FunctionName = varRetVal
End Function
The actual code you provide for each branch of the Select Case statement will vary depending on the specific requirements of your custom function, but the basic form of the function will always look like that shown here.
Setting the RowSourceType Property
The last thing to understand is how to properly specify a custom function in the RowSourceType property setting. Whether you set the property using the property sheet or using Visual Basic for Applications (VBA) code, you need to provide only the name of the custom function. The function name is entered without a preceding equal sign (=) and without the trailing pair of parentheses.
Custom Function Examples
The following sections contain some sample custom functions to illustrate how to implement the function template form described in the preceding section.
Filling a Control with a Calculated Value
The following function returns a list of the next four Mondays following today's date. To call this function from a combo box or list box control, you enter ListMondays as the RowSourceType property setting and leave the RowSource property setting blank. The control will display four rows of data in one column, using the default column width for the control.
Since acLBGetFormat and acLBEnd are not used in this example, no code needs to be provided for those conditions.
Function ListMondays(fld As Control, id As Variant _
Row As Variant, col As Variant, _
code As Variant) As Variant
Dim intOffset As Integer
Dim varRetVal As Variant
Select Case code
Case acLBInitialize
' Just return True here.
varRetVal = True
Case acLBOpen
' Use the Timer function to generate a unique value.
varRetVal = Timer
Case acLBGetRowCount
' Create space for 4 rows of data.
varRetVal = 4
Case acLBGetColumnCount
' Create a single column of data.
varRetVal = 1
Case acLBGetColumnWidth
' Use the control's default column width.
varRetVal = -1
Case acLBGetValue
' Use the row argument to generate different dates
' for different rows.
intOffset = Abs((9 - Weekday(Now))Mod 7)
varRetVal = Format(Now() + intOffset + 7 _
* row, "mmmm d")
Case acLBGetFormat
Case acLBEnd
End Select
ListMondays = varRetVal
End Function
The property sheet for the combo box control that calls this function looks like this:
Filling a Control with Disk File Information
The next example uses a static array to store the names of the databases in the current directory. To call this function, you enter ListMDBs as the RowSourceType property setting and leave the RowSource property setting blank. The control will display a row of data for each database found (up to 50) in one column, using the default column width for the control.
Function ListMDBs(fld As Control, id As Variant _
Row As Variant, col As Variant, _
code As Variant) As Variant
Static strMDBNames(50) As String
Static intEntries As Integer
Dim varRetVal As Variant
Select Case code
Case acLBInitialize
' Use the Dir function to fill the static array with
' the names of the databases found on in the current
' directory. If none are found then varRetVal = 0
' and the procedure terminates.
intEntries = 0
strMDBNames(intEntries) = Dir("*.mdb")
Do Until strMDBNames(intEntries) = "" _
Or intEntries >= 50
intEntries = intEntries + 1
strMDBNames(intEntries) = Dir
Loop
VarRetVal = intEntries
Case acLBOpen
' Use the Timer function to generate a unique value.
varRetVal = Timer
Case acLBGetRowCount
' There should be a row for each found file.
varRetVal = intEntries
Case acLBGetColumnCount
' Create a single column of data.
varRetVal = 1
Case acLBGetColumnWidth
' Use the control's default column width.
varRetVal = -1
Case acLBGetValue
' Get the database name from the array.
varRetVal = strMDBNames(row)
Case acLBGetFormat
Case acLBEnd
' All finished so clear out the array.
Erase strMDBNames
End Select
ListMDBs = varRetVal
End Function
Notice that in this function the array is filled with data in the acLBInitialize case, and the count of found files is saved in the intEntries variable. The value of intEntries is later used to specify the number of rows when the acLBGetRowCount case occurs. The values in the static array are later accessed when the acLBGeValue case occurs, using the row argument supplied by Access as the index into the array for each data item. Notice also that if the Dir function finds no matching files in the current directory, intEntries will equal 0. If 0 (or Null) is returned when acLBInitialize is called, Access simply stops calling your function, and no data is displayed in the control.
Filling a Control with Database Objects
It should be apparent by now that the real key to this function is getting data into the static array in the acLBInitialize case, and then accessing that array in the acLBGetValue case. The following example fills a combo box with the names of all the tables in the current database. With the exception of the portions of sample code shown here, this function would be identical to the function shown in the previous example. In addition, you would have to set a reference to the Microsoft ADO Extensions 2.1 for DDL and Security (ADOX) type library.
The final example uses a static array to store the names of the tables in the current database. To call this function, enter ListTables as the RowSourceType property setting and leave the RowSource property setting blank. The control will display a row of data for each table in the database (up to 256) in one column, using the default column width for the control.
Function ListTables(fld As Control, id As Variant _
Row As Variant, col As Variant, _
code As Variant) As Variant
Dim cat As New ADOX.Catalog
Dim tbl As ADOX.Table
Static tbls(256) As String
Static intEntries As Integer
Dim varRetVal As Variant
varRetVal = Null
Select Case code
Case acLBInitialize
cat.ActiveConnection = CurrentProject.Connection
intEntries = 0
For Each tbl In cat.Tables
If tbl.Type == "TABLE" Then
If tbl.Name <> "dtproperties" Then
tbls(intEntries = tbl.Name
intEntries = intEntries + 1
End If
End If
varRetVal = intEntries
' the remaining code would be the same as
' in the previous example.
ListTables = varRetVal
End Function
Just as in the previous example, the acLBInitialize case is used to get the data you want to display in the control and preserve it so that it will be available during later calls to the function for the acLBGetCalue case.
Once you understand the format for the custom function, it is easy to see how this alternative means of filling a combo box or list box control can be quite powerful.
Where to Get More Info
Here are some additional resources you can use to explore working with Microsoft Access:
- For more information on developing solutions using Microsoft Access see the Office Developer site's list of recommended articles (https://msdn.microsoft.com/office).
- As always, check in regularly at the Office Developer site for information and technical articles (https://msdn.microsoft.com/office) on Office solution development.
David Shank is a Programmer/Writer on the Office team specializing in developer documentation. Rumor has it he lives high in the mountains to the east of Redmond and is one of the few native northwesterners still living in the northwest.