Using the ListView ActiveX Control
by Mark Bradley, 27th April 2001


Purpose

This document is aimed at assisting programmers who are wishing to implement the Microsoft Visual Basic ActiveX ListView Control in their applications. Methods of creating the ListViews, setting up the column headers and other properties, filling the lists with data from rowsets, adding and removing elements, saving the list items back to a rowset, navigating through the elements in the lists and printing the lists are demonstrated using code examples from a working application.

Acknowledgements

I would like to thank Ken Mayer, Gary White, Jean-Pierre Martel, Dan Howard, Bowen Moursund, Vic McClung, David L. Stone (my proof-reader) and others in the dBASE Newsgroups for their suggestions, code samples and generous help.

Why Use ListView?

Some time ago, I created a form that allowed a user to transfer all or selected values in two arrays from a source Listbox to a destination Listbox and vice versa. I wanted the user to be able to switch the elements back and forth until satisfied before actually saving the data back to the rowsets.

I borrowed heavily from Ken Mayerís Mover.cc program to accomplish this.

Recently, I needed to create another form to do likewise but this time I needed the Listboxes to display more than one column of data and to be able to horizontally scroll across the lists. The in-built Visual dBASE Listbox (up to Version 7.5 anyway) is unable to do this.

After much research on the subject, I decided to give the Visual Basic ActiveX control ListView a go, based on a suggestion from Gary White.

Benefits of Using ListView

Getting Started

The ListView control is part of a group of ActiveX controls that are found in the MSCOMCTL.OCX file. To use the ListView control in your application, you must add the MSCOMCTL.OCX file to the project. When distributing your application, install the MSCOMCTL.OCX file in the userís Microsoft Windows System or System32 directory.

Using the Set up ActiveX Components option of the Visual dBASE Component Palette, add the Microsoft ListView Control component to your palette. Another article in the dBASE Developers Bulletin has covered this subject thoroughly enough for me to not have to repeat the process in detail here.

Create a form, drop the ListView component onto it and youíre then ready to start programming it!

About the Example Form

First, to help you understand the code samples in this document, here is some background information on the form and itís associated tables used in the example code.

The purpose of the form is to allow a user to display all the qualifications issued to employees and to issue and remove these qualifications as required. The user may optionally print qualification forms and certificates as they are issued and also print lists of qualifications issued or not issued to the employees.

There are four types of qualifications: Appointments, Authorisations, Skills and Learner Permits. The user can choose to have the source list display the entire list of the qualifications or just those qualifications assigned to the employeeís job title.

The form contains a three-page Tabbox. The first page lets the user operate on one employee and his/her issued or not-issued qualifications:

 The second page of the Tabbox lets the user operate on one qualification and all the employees issued/not issued with it:

 The third page of the Tabbox lets the user operate on ALL the qualifications of each type and ALL the employees issued with them:

On the first and second pages of the form, there are two ListView components called SourceBox and DestinationBox respectively, while on the third page, the DestinationBox ListView is re-sized to cover the width of the form while the SourceBox ListView is hidden.

The relevant tables and their fields used by the form are:
 
  Table Name Field Name Purpose
  Details.dbf (Employee details) Emp_no Employee Number
    Surname Surname
    Chrst_name Given Name
    Jobname Job Title
  Appntcod.dbf (Qualifications Lookup) Appointmnt Qualification Title
    Key_Code Qualification Type Qualifier
  Appdet.dbf (Employee Qualifications) Appointmnt Qualification Title
    Key_Code Qualification Type Qualifier
    Last_Used Date Stamp
  Qualjobs.dbf (Job Title Qualifications) Key_Code Qualification Type Qualifier
    Appointmnt Qualification Title
    Jobname Job Title
       

Creating the ListView Components

If you have successfully added the ListView component(s) to the form, the underlying code should look something like this:
 
 
this.SOURCEBOX = new ACTIVEX(this)
with (this.SOURCEBOX)
   height = 8.86
   left = 2
   top = 2.4545
   width = 43.4286
   pageno = 0
   license = "9368265E-85FE-11d1-8BE3-0000F8754DA1"
   state = "DCFEEFCDBJBBBBBBBDCQBBBBMPCEB..."  // Truncated
   classId = "{BDD1F04B-858B-11D1-B16A-00C0F0283628}"
endwith
   

I added the following two events to the ActiveX Control:
 
 
with (this.SOURCEBOX.nativeObject)
   DblClick = class::MOVECURRENTRIGHTBUTTON_ONCLICK
   ColumnClick = class::NATIVEOBJECT_COLUMNCLICK
endwith

this.DESTINATIONBOX = new ACTIVEX(this)
with (this.DESTINATIONBOX)
   height = 8.8636
   left = 53.4286
   top = 2.4545
   width = 43.4286
   pageno = 0
   license = "9368265E-85FE-11d1-8BE3-0000F8754DA1..."  // Truncated
   classId = "{BDD1F04B-858B-11D1-B16A-00C0F0283628}"
endwith

   

I added the following two events to the NativeObject of the ActiveX control:
 
 
with (this.DESTINATIONBOX.nativeObject)
   DblClick = class::MOVECURRENTLEFTBUTTON_ONCLICK
   ColumnClick = class::NATIVEOBJECT_COLUMNCLICK
endwith
   

Adding the Column Headers and Customising the ListViews

Because my program needs to vary the Column Headers at runtime, I created a function called Prepare_Set() which is called at various times by certain user actions. Prepare_Set() sets up the ListViews appropriate to what the user is doing.

Therefore, this is the logical place to set up the Column Headers and other features. Here is a part of the code for Prepare_Set() which sets up the ListViews when page one of the Tabbox is selected.

Note that you must use the NativeObject when referring to the in-built properties of the control:
 
 
Function Prepare_Set

   // Set up the Source and Destination ListView controls

   // Initialise a variable «x» as a reference to the SourceBox properties
   Source_Box =  form.SourceBox.nativeobject

   // Clear the list of all existing elements and column headers (thanks Gary White)
   Source_Box.listitems.clear()
   Source_Box.columnheaders.clear()

   // Set the various Visual Basic properties for the ListView

   // Disable user editing of the elements
   Source_Box.LabelEdit = 1 

   // Enable sorting in the ListView
   Source_Box.sorted = True

   // Set the ListView output to «report» style
   Source_Box.view = 3

   // Make the highlight bar stretch across the whole row
   Source_Box.FullRowSelect = True

   // Don't hide whatever the user selects
   Source_Box.HideSelection = False

   // Turn the highlight bar another colour when the mouse cursor is on it
   Source_Box.HotTracking = True

   // Allow or donít allow the highlight bar to «follow» the mouse cursor (I found it better off)
   Source_Box.HoverSelection = False

   // Allow the user to re-arrange the column order
   Source_Box.AllowColumnReorder = True

   // Same for the Destination ListView
   Dest_Box =  form.DestinationBox.nativeobject
   Dest_Box.listitems.clear()
   Dest_Box.columnheaders.clear()
   Dest_Box.LabelEdit = 1 
   Dest_Box.sorted = True
   Dest_Box.view = 3
   Dest_Box.FullRowSelect = True
   Dest_Box.HideSelection = False
   Dest_Box.HotTracking = True
   Dest_Box.HoverSelection = False
   Dest_Box.AllowColumnReorder = True

   // If page one of the Tabbox is selected
   If Form.TABBOX1.curSel = 1

      // Assign Column Headers where
      // the first parameter is the column number, the second is a unique key field,
      // the third is the column header title text and the fourth is the column width
      Source_Box.columnheaders.Add(1, "Title", "Title", 280)

      // Assign which column to initially sort by (0 = the main item itself rather than subitems)
      Source_Box.SortKey = 0

      // Repeat for the DestinationBox, this time adding two columns
      Dest_Box.columnheaders.Add(1, "Title", "Title", 175)
      Dest_Box.columnheaders.Add(2, "Date", "Date", 100)
      Dest_Box.SortKey = 0

      // Populate the Source ListView control with data from a rowset
      rQ = Form.datamodref1.ref.qualifications.rowset
      If rQ.first()
         do while not rQ.EndOfSet
            rQ.next()
            cAppointment = rQ.fields[ "Appointmnt" ].value
            itmx = Source_Box.ListItems.add(1,"",cAppointment)
            rQ.next()
         Enddo
      Endif

     // Populate the Destination ListView control with data Ė this is more complex as it
     // only adds elements that are not already in the SourceBox. Note the word «ListSubItems»
     // is used when referring to columns other than the main one. The Visual basic reference
     // calls them the «SubItems» property but not so when used in VDB for some reason.
     // Thanks again to Gary White for pointing this out to me after hours of lost time!
     rD = Form.datamodref1.ref.details.rowset
     If Form.rowset.findkey( rD.fields["Emp_no"].value+Form.aCode )
        Do while not ... // Truncated
           cAppointment = Form.rowset.fields[ "APPOINTMNT" ].value
           cLast_Used = DTOC(Form.rowset.fields[ "LAST_USED" ].value)
           itmx = Dest_Box.ListItems.add(1,"",cAppointment)
           itmxs = itmx.listsubitems.add()
           itmxs.text = cLast_Used
           Form.rowset.next()
        Enddo 
     Endif
   Endif

   

When dBASE instantiates an ActiveX control, this control becomes a dBASE object. However, that object still has the properties, methods and functions set by its developer. In the case of the ListView control, its Visual Basic roots are revealed in the three lines of code above shown in red. For a dBL developer unfamiliar with the Visual Basic language, this piece of code is rather un-orthodox. So letís give some explanation. The first of these lines adds a ďparentĒ row to the Listbox. This must contain a value (in this case, x). The second line adds a sub-item to the parent row (on other words, another column). The third line assigns the value of y to this sub-item.

Moving the Elements Back and Forth

As in Ken Mayerís Mover.cc there are 4 ways to move the elements: Move all left, Move selected left, Move all right and Move selected right. Below are some snippets from the MoveCurrentLeft() function:
 
 
Function MoveCurrentLeftButton_onClick

   // Move the currently selected item in DestinationBox back to the SourceBox
   // (this is called both from a left-double-click on the listview and from a pushbutton):

   // First, check to see if weíre working with an empty list
   Dest_Box = form.DestinationBox.nativeobject
   If Dest_Box.ListItems.count == 0
      MsgBox("List is empty, nothing to remove.",[For your information ...],16)
      return
   Endif

   // Now move just the currently selected element:

   // This bit of code sets the variable p to the index of the next item
   // to be auto-selected once the selected item is removed. This was
   // harder than it looks, then again, Iím getting old!.
   i = Dest_Box.SelectedItem.Index
   If Dest_Box.ListItems.count > 1
      If i = Dest_Box.ListItems.count
         p = i - 1
      Else
         p = i
      Endif
   Else
      p = 0
   Endif

   // Assign the variable x to the text of the selected item
   x = Dest_Box.ListItems(i).text

   // Create a new element in the SourceBox ListView and make itís value
   // equal to the item being moved
   Source_Box = form.SourceBox.nativeobject
   itmx = Source_Box.ListItems.add(1,"",x)

   // Now remove the unwanted element from the DestinationBox
   Dest_Box.ListItems.Remove(i)

   // And finally, Auto-Select the next item in the DestinationBox
   If p > 0
      j = Dest_Box.ListItems(p)
      j.EnsureVisible()
      j.Selected = True
   Endif
return

   

Programmatically Changing the Values of Elements in the ListViews

Attempting to do this successfully the Visual Basic way caused me many, many hours of chagrin and frustration. I could change the values without error using the method explained in the Visual Basic Reference but could never get the ListViews to actually display the changed data.

Finally I came up with a work-around that firstly saves the data in the element that is to have itís ListItem and/or ListSubItem changed programmatically. It then removes the element and immediately adds it again with the changed value(s). Ugly, but it works good!

The following function changes a date value in one of the ListSubItems at the userís request.
 
 
Function UpdatePushbutton_onClick

   Dest_Box = Form.DestinationBox.nativeobject
   If Dest_Box.ListItems.count == 0
      MsgBox("Issued Listbox is empty, nothing to update.",[For your information ...],16)
      return
   Endif

   // Assign variable p to the next item in the list for later auto-selection
   i = Dest_Box.SelectedItem.Index
   If i < Dest_Box.ListItems.count
      p = i + 1
   Else
      p = 1
   Endif

   // Update just the element selected:
   < TRUNCATED - Put code here to update the rowset with the new date >

   x = Dest_Box.ListItems(i).text
   y = DTOC(form.UpDaterSpinbox.value)
   Dest_Box.ListItems.Remove(i)
   itmx = Dest_Box.ListItems.add(1,"",x)
   itmxs = itmx.listsubitems.add()
   itmxs.text = y

   // Auto-Select the next item in the list
   j = Dest_Box.ListItems(p)
   j.EnsureVisible()
   j.Selected = True
return

   

Saving the Data in the Lists Back to the Rowsets

Here is some code from the «Save» button onClick() event of the form. This is invoked by the user when finished moving the elements back and forth to get the final required result. The routine firstly checks the rowset for data not contained in the DestinationBox and deletes the offending rows. Then it adds any new data in the DestinationBox back to the rowset.
 
 
Function SaveButton_onClick

   // Delete any records not in the Destination List
   Dest_Box = Form.DestinationBox.nativeobject
   j = form.Rowset.count()
   Form.rowset.findkey(...)  // Truncated
   Do While ...  // Truncated
      x = Form.rowset.fields[ "APPOINTMNT" ].value
      nRow = Dest_Box.FindItem( x, 0 )
      If nRow = 0
         Form.rowset.delete()
      Else
         Form.rowset.next()
     Endif
   Enddo

   // If the Destination List is empty, fuggettaboudit !
   If Dest_Box.ListItems.count == 0
      Form.SAVEBUTTON.enabled = false
      Form.ABANDONBUTTON.enabled = false
      Form.Listcount()
      return
   Endif

   // Check each item in the Destination List
   // to see if it's a record in the rowset.
   // If not, add it!
   For i = 1 to Dest_Box.ListItems.count
      If not form.rowset.findkey(<TRUNCATED>.nativeobject.ListItems(i).text )
         Form.rowset.beginAppend()
         Form.rowset.fields["Key_Code"].value = Form.aCode
         Form.rowset.fields["Appointmnt"].value = <TRUNCATED>.fields["Appointmnt"].value
         Form.rowset.fields["Emp_No"].value = <TRUNCATED>.fields["Emp_no"].value
         Form.rowset.fields["Last_Used"].value = Form.UPDATERSPINBOX.value
         Form.rowset.save()
      Endif
   Next

   

Allowing the User to Sort the Elements

Here is the function referred to in the constructor code for the ListViews which lets the user sort the lists:
 
 
Function NativeObject_ColumnClick(ColumnHeader)
   this.SortKey = ColumnHeader.Index-1
   If this.listItems.count <> 0
      this.SelectedItem.EnsureVisible()
   Endif
return
   

Printing the Lists

This function uses temporary tables created in the formís DataModule to store the elements in the ListViews before calling a standard report form to print them. There are probably much better ways to do this, perhaps using Vic McClungís Printer Class but, cíest la guerre!

The code in the DataModule looks like this:
 
 
this.PAGEONE = new QUERY()
this.PAGEONE.parent = this
with (this.PAGEONE)
   onClose = class::PAGEONE_ONCLOSE
   execute = class::PAGEONE_EXECUTE
   left = 2.5714
   top = 5.1818
   session = form.session1
   database = form.database1
   sql = ""
   active = true
endwith

function PageOne_execute
  _app.PageOneTable = funique("TMP?????.dbf")
   x = "create table '" + _app.PageOneTable + "' ( Appointmnt char(50), Last_used Date ) "
   &x
   this.sql = "select * from " + _app.PageOneTable
return QUERY::execute()

   

The function that prints the lists looks like this:
 
 
Procedure ReportButton_onClick

   // First, empty the PageOne temporary table
   Rowsetz = Form.datamodref1.ref.pageone.rowset
   j = Rowsetz.count()
   Rowsetz.first()
   Do while not Rowsetz.EndOfSet
      Rowsetz.delete()
   Enddo

   // If the user has chosen the DestinationBox List to be printed
   Dest_Box = Form.DestinationBox.nativeobject
   If Form.ISSUEDLIST.value
      If Dest_Box.ListItems.count == 0
         MsgBox("Issued List is empty - nothing to print!",[For your information ...],16)
         return
      endif
      For Num = 1 To Dest_Box.ListItems.Count
         x = Dest_Box.ListItems(Num).text
         y = Dest_Box.ListItems(Num).listSubItems(1).text
         Rowsetz.BeginAppend()
         Rowsetz.fields[ "Appointmnt" ].value := x
         Rowsetz.fields[ "Last_Used"  ].value := ctod(y)
      Next
      Rowsetz.first()
   Else

   // Duplicate the above for the SourceBox List
   Endif

   // Now print the damn thang
   Do frontend.wfm with "Issquals.rep"

   

Selecting the First Item in the ListViews

These functions are called quite often whenever the lists have been refreshed.
 
 
Function SelectFirstDest

   // Select the 1st item in the Destination List
   Dest_Box = Form.DestinationBox.nativeobject
   If Dest_Box.ListItems.Count > 0
      elem = Dest_Box.ListItems(1)
      elem.EnsureVisible()
      elem.Selected = True
   Endif

Function SelectFirstSource

   // Select the 1st item in Source List
   Source_Box = Form.SourceBox.nativeobject
   If Source_Box.ListItems.Count > 0
      elem = Source_Box.ListItems(1)
      elem.EnsureVisible()
      elem.Selected = True
   Endif

   

Displaying the Number of Elements in Each ListView

This little function continually updates the count of elements in each ListView as elements are added and removed. The sort of thing users appreciate!
 
 
Function ListCount
  Source_Box = Form.SourceBox.nativeobject
  Dest_Box = Form.DestinationBox.nativeobject
   newstring = substr(Form.SOURCETITLE.text,1,at("(",Form.SOURCETITLE.text))
   Form.SOURCETITLE.text = newstring + ;
   ltrim(rtrim(str(Source_Box.ListItems.count))) + " records)"
   newstring = substr(Form.DESTTITLE.text,1,at("(",Form.DESTTITLE.text))
   Form.DESTTITLE.text = newstring + ;
   ltrim(rtrim(str(Dest_Box.ListItems.count))) + " records)"
return
   

Conclusion

My overriding need to use the ListView was to have the capability of displaying multiple columns from tables in the list and to be able to scroll across the columns. I also needed a replacement for the rather ill-behaving VdB Grid that would also allow me to display multi-dimensional array data.

The ListView control has given me all these advantages and more in a well-behaved, very programmable, flexible and attractive control.

Clients who have used the control in my applications have remarked on the speed and user-friendliness of the control compared to earlier versions of the same applications that used single column lists or cumbersome grids.

For further information on the ListView Control itself, visit the Visual basic Reference site at: http://msdn.microsoft.com/library/default.asp?URL=/library/devprods/vs6/vbasic/vbcon98/vbstartpage.htm


Note: The author would like to thank David L. Stone, his proof-reader, for the improvements he brought to this text.