30 March 2009

Sort a ListView Ocx

To set the scene you might want to look at the knowledge base about sorting a ListView control in (Visual) BASIC http://support.microsoft.com/kb/170884. GFA-BASIC 32 supports only one method to sort a column, where VB uses three (!) properties to get a ListView control sorted. The .Sort method is defined as: ListView.Sort column%, compare% The .Sort method of the control only sorts the items alphabetically, either ascending or descending. The column% argument specifies the column to sort, but in contrast with the ColumnHeaders collection it starts counting at 0, rather than at 1. I have no idea why, but the column% parameter is passed to the LVM_SORT message directly. Ok, well so be it. The compare% argument is a bit more complex, because it is used as a bit flag. When the lowest bit of the high order word is set ($10000), the column is sorted descending (Z-A), otherwise ascending (A-Z). The low-word of the compare% argument specifies the compare mode GFA-BASIC 32 will use when comparing one item to another. The values are the same as for Mode Compare. For example, to sort the second column in a descending way using uppercase comparison (Mode Compare = -2), you would use: ListView.Sort 1, $10000 Or (-2) A bit complicated. Maybe the Sort method should have used the Mode Compare setting rather than letting it be specified in an argument. Anyway, a column may be sorted comparing using lowercase or using uppercase separately from the global setting. Custom Sort A column doesn't always consist of text string entries that have to sort alphabetically. A column may consist of dates or numbers (integers or floats). The .Sort method isn't prepared for sorting anything different than text. The link to the knowledge base opens an article that discusses how to create a custom sort for dates for a VB ListView control. Although, GFA-BASIC 32 tries to be VB compatible, the ListView Ocx control differs in many aspects. (The previous post discusses how the ListItems collection differs from VB.) GFA-BASIC 32 stores a pointer to the ListItem object in the lParam member of the LV_ITEM structure when it adds an item to the ListView Ocx. VB stores the ListView's item index. This difference is important because of the custom compare function to write for the custom sort. To initiate a custom sort you must send the LVM_SORT message to the ListView. Most often you will do this after a click in the column header. In GFA-BASIC 32 you might see the following code, ported from the code presented in the MS knowledge base article:
' GFA32 argument ByRef, not by value!
Sub ListView1_ColumnClick(ColumnHeader As ColumnHeader)
  Dim lngItem As Long
  ' Note:
  ' ColumnHeaders collection is 1-based
  ' ListItem.SubItems(iCol) is 0-based
  ' ListView.Sort iCol is 0-based

  Dim iSubItem As Long = ColumnHeader.Index - 1 ' 1-based

  'Handle User click on column header
  If ColumnHeader.Text = "Name" Then  'User clicked on Name header
    'ListView1.Sorted = True        'Use default sorting to sort the
    'ListView1.SortKey = 0          'items in the list
    ListView1.Sort iSubItem, 0          ' 0-based
  Else
    'ListView1.Sorted = False       'User clicked on the Date header
    'Use our sort routine to sort by date
    SendMessage ListView1.hWnd, LVM_SORTITEMS, iSubItem, ProcAddr(CompareDates)
  End If

  'Refresh the ListView before writing the data
  ListView1.Refresh

  ' MS/VB
  'Loop through the items in the List to print them out in sorted order.
  'NOTE: You are looping through the ListView control because when
  'sorting by date the ListItems collection won't be sorted.

  For lngItem = 1 To ListView1.ListItems.Count
    'ListView_GetListItem lngItem, ListView1.hWnd, strName, dDate
    'Print ListView1.ListItems(lngItem).AllText
  Next

  ' GFA32 - The ListItems collection is sorted as well.
  Dim li As ListItem
  For Each li In ListView1.ListItems
    Print li.AllText
  Next

End Sub
To show the differences with VB the code is heavily commented. The clue is located in the next code line:
SendMessage ListView1.hWnd, LVM_SORTITEMS, iSubItem, ProcAddr(CompareDates)
The .hWnd property returns the window handle of the ListView Ocx. The wParam argument specifies the corrected column number and the lParam argument holds the pointer to the function to compare two ListView items. The CompareDates function is declared as:
Function CompareDates(ByVal lngParam1 As ListItem, _
  ByVal lngParam2 As ListItem, ByVal iCol As Long) As Long
The first two long integer arguments receive the lParam member of the LV_ITEM structure, which is in GFA-BASIC 32 a pointer to a ListItem object. The compare function is a callback function. It is called from inside Windows and never from a GFA-BASIC 32 statement. GFA-BASIC 32 cannot check the function parameters against the data types passed when called. When the program is executing the lParam members are simply copied to the first arguments of the compare function. Because we know the value points to a ListItem object, we cast the 32-bits value to a Listitem. The GFA-BASIC 32 compare function is much simpler, convert the text of the ListItems to apropriate data type and compare:
Function CompareDates(ByVal lngParam1 As ListItem, _
  ByVal lngParam2 As ListItem, ByVal iCol As Long) As Long

  Dim dDate1 As Date, dDate2 As Date

  ' MS/VB way:
  'Obtain the item names and dates corresponding to the
  'input parameters
  °ListView_GetItemData lngParam1, hWnd, strName1, dDate1
  °ListView_GetItemData lngParam2, hWnd, strName2, dDate2

  ' GFA32 way. The LV_ITEM.lParam is passed to the compare
  ' function. The lParam contains the address of the ListItem
  ' object, which we intelligently casted to a ListItem object!
  dDate1 = ValDate(lngParam1.SubItems(iCol))
  dDate2 = ValDate(lngParam2.SubItems(iCol))

  'Compare the dates
  'Return 0 ==> Less Than
  '       1 ==> Equal
  '       2 ==> Greater Than

  If dDate1 < dDate2 Then
    CompareDates = 0
  ElseIf dDate1 = dDate2 Then
    CompareDates = 1
  Else
    CompareDates = 2
  End If

End Function

No comments:

Post a Comment