Difference between revisions of "Listview"

From Team Developer SqlWindows Wiki
Jump to: navigation, search
(Added tip on how to set cListView font at runtime)
(Added article on listview custom sorting)
 
Line 38: Line 38:
 
|TEXT=WIKI_cListView_SetFontAtRuntime.zip
 
|TEXT=WIKI_cListView_SetFontAtRuntime.zip
 
}}
 
}}
 +
 +
 +
 +
<!------------------------------------------------------------------------------------------------------------------------------>
 +
{{TipHeader|cListView: custom sorting}}
 +
The cListView custom control is a wrapper around the "SysListView32" WinApi control.<br>
 +
The data displayed can be sorted, but it is always based on a string sort.
 +
 +
 +
This can result in unwanted sorting order. For instance, when a column is populated with numbers or dates, depending on<br>
 +
how the data is formatted, will display the data in the wrong sequence.
 +
 +
 +
Have a look at the next screenshot. The number column and date column should be sorted in ascending order, but clearly the data is not<br>
 +
sorted on the number and date values we would like.
 +
 +
 +
[[Image:CListView_DefaultSorting.png]]
 +
 +
 +
This forces us to format the data in such a way they will be correctly sorted. For instance placing prefixed zero's on numbers or<br>
 +
a specific date format having year in front, then month and then the day.<br>
 +
But this is certainly not ideal and in most cases even unacceptable to the users.
 +
 +
 +
There are two ways to remedy this:
 +
 +
*Sort the listview on another column which is not visible but has the data formatted correctly. The so called "mirror" sorting
 +
*Implement the sorting routine yourself. So overrule the default listview string sorting and replace it with the sorting you like. So custom sorting
 +
 +
 +
'''Mirror sorting'''
 +
 +
 +
In this solution you populate the listview with extra columns having the needed formatted data. When sorted will result in the correct sequence.<br>
 +
So, the column which holds the wanted display formatting is not the column to sort on, but the sorting is done on another column.<br>
 +
Such a column should be invisible to the user.
 +
 +
 +
One drawback: listviews do not support hiding of columns. On tables for example, we can implement hiding of certain columns and populate data we like.<br>
 +
Unseen by the user. Unfortunately, hiding of columns on listviews is not possible.
 +
 +
 +
A common trick is to set the width of the column to zero (0). But that will not prevent the user to drag the column divider and resize the "hidden" column into view.<br>
 +
But this might be enough for your project to implement. A column having a width of zero pixels is invisible to the user.
 +
 +
 +
In this solution we need to trap the click on the column header (the sort action), ignore the event so that the default sorting is not performed and execute the sort on the column we have "hidden".
 +
 +
 +
The listview component sends WM_Notify messages to the control to inform specific actions. A notify structure is send along with it containing all info of the event.<br>
 +
We need to read the contents of this notify structure, detect that a column was clicked and, based on the column index, decide to ignore the sorting and perform the sort on another column.<br>
 +
 +
 +
The details how to do this can be found in the sample. One thing to know is that you have to return FALSE on the WM_Notify message to inform the control that sorting is not needed.<br>
 +
After this, call the listview sort on the column you want.
 +
 +
 +
'''Custom sorting'''
 +
 +
 +
The WinAPI listview control has a feature to be able to implement your own sort logic.<br>
 +
When you send the message '''LVM_SORTITEMSEX''' to the listview control, it will start a custom sort.<br>
 +
It does this using a callback function. The listview will call this callback function for each item in the listview.<br>
 +
The callback function will be called with two (2) item indexes. The function should return one of three (3) values:
 +
 +
 +
*Return a negative value (-1) if the first item should precede the second
 +
*Return a positive value (+1) if the first item should follow the second
 +
*Return zero (0) when the items are equal
 +
 +
 +
So, based on the item index, you need to identify which data this represents. For instance, we should know that item on index 1 is a specific date.<br>
 +
The second item, on index 2, has its own date.
 +
 +
 +
We should then compare the two dates and return -1, 0 or 1 depending on the date value. We are in charge how to do the compare.<br>
 +
The listview will continue calling the callback function for the items and knows in the end how to sort the column.
 +
 +
 +
But, and now the unfortunate part, Team Developer does not support callback functions. A callback function is a pointer to a function, and TD does not offer this feature.<br>
 +
Fortunately, we have the great TD component, called '''Callback'''. Created by Christian Schubert.
 +
This handy DLL gives TD the callback feature we need and is available for all TD versions.
 +
 +
 +
To register a callback function and get a pointer to be used in '''LVM_SORTITEMSEX''' is done using this:
 +
 +
 +
<pre>
 +
Call SetCallback( "__CustomSortCompareFunc", 3, FALSE, nCallbackFunctionPtr, nCallbackProcessHandle )
 +
</pre>
 +
 +
 +
What we are doing here is indicating which TD function acts like a callback function (the first parameter) and we will get back a function pointer.<br>
 +
In this case we pass in the function '''"__CustomSortCompareFunc"'''. This is a real function which should be implemented in your code.<br>
 +
We get the function pointer back in the fourth (4) parameter, '''nCallbackFunctionPtr'''.
 +
 +
 +
Having a function pointer, we can pass it as '''lParam''' when sending the '''LVM_SORTITEMSEX''' message to the listview control:
 +
 +
 +
<pre>
 +
Call SalSendMsg( GetWindow( hWndItem, GW_CHILD ), LVM_SORTITEMSEX, 0, nCallbackFunctionPtr )
 +
</pre>
 +
 +
 +
We have to send the message to the actual WinApi control and not the TD wrapper. To get the window handle of the WinApi control:
 +
 +
 +
<pre>
 +
GetWindow( hWndItem, GW_CHILD )
 +
</pre>
 +
 +
 +
(The WinApi control is the child of the cListView control)
 +
 +
 +
The only thing to do now, is to implement the callback function itself. It should have the exact name you specified in the '''SetCallback''' function:
 +
 +
 +
<pre>
 +
Function: __CustomSortCompareFunc
 +
  Returns
 +
      Number:
 +
  Parameters
 +
      Number: pnItemIndex1
 +
      Number: pnItemIndex2
 +
      Number: pnValue
 +
  Actions
 +
      ! Find out the real data of pnItemIndex1 and pnItemIndex2
 +
      ! Then compare the real item data and return the needed value
 +
 +
      If item1 < item2
 +
        Return -1
 +
      Else If item1 > item2
 +
        Return 1
 +
      Else
 +
        ! Items are equal
 +
        Return 0
 +
</pre>
 +
 +
 +
This function will automatically be called by the listview control until all items are compared.<br>
 +
After this, the column will be sorted using the sorting order you specified in the callback function.<br>
 +
The way you compare could be very complex, using multiple values from the real item. Any sort order for every kind of data can be implemented.
 +
 +
 +
'''Sample application'''
 +
 +
 +
To show both sorting methods, the mirror sorting and the custom sorting, a little sample has been created.<br>
 +
The archive contains the callback component for each TD version. So before starting the sample, be sure to copy the needed callback dll and library to the main folder of the sample.<br>
 +
Also uncomment the library include for callback in the libraries section.
 +
 +
 +
When starting the sample you have this screen:
 +
 +
 +
[[Image:CListViewSortingSample.png]]
 +
 +
 +
Initially the listview is sorted on the first column, "Number (default)".
 +
It is sorting using the default string sort method. The column is clearly not in the right order.
 +
 +
 +
When clicking on the column headers, you can sort them:
 +
 +
 +
*The columns "Numbers (default)" and "Dates (default)" will sort the standard way
 +
*The columns "Numbers (custom)" and "Dates (custom)" will do a custom sort using the callback
 +
*The columns "Numbers (mirror)" and "Dates (mirror)" will do a mirror sort using the "hidden" extra columns
 +
 +
 +
The checkbox "Show hidden columns" will toggle the mirrored columns widths so you can see how the formatted data looks like.
 +
 +
 +
The buttons will execute a sort programmatically.<br>
 +
So the sample does not only support sorting when clicking on the column header, but you can execute a mirror and custom sort at any time in your code.<br>
 +
 +
 +
Here you can download the sample:<br>
 +
{{Download
 +
|URL=http://samples.tdcommunity.net/index.php?dir=&file=WIKI_cListView_CustomSorting.zip
 +
|TEXT=WIKI_cListView_CustomSorting.zip
 +
}}
 +
 +
  
  

Latest revision as of 18:33, 28 December 2018

VT ListView control


Contents


Pointer2.png cListView: set font at runtime Pointer.png

Using SalFontSet on a cListView does not work, the object will not change the font at runtime.
Reason is that the cListView object is actually a wrapper around the "real" "SysListView32" WinApi control.


See the image below which shows the architecture using TDAppTools GuiInspector


CListView GuiInspector.png


We need the window handle of the "SysListView32" WinApi control, which is a child of cListView object.
Using the following we obtain the window handle:

hWndSysListView32 = GetWindow( cListViewOriginal, GW_CHILD )


Using the SysListView32 handle, use SalFontSet to change the font

Call SalFontSet( hWndSysListView32, "Arial", 12, FONT_EnhNormal )


Here you can download a sample:
Down.png WIKI_cListView_SetFontAtRuntime.zip


Pointer2.png cListView: custom sorting Pointer.png

The cListView custom control is a wrapper around the "SysListView32" WinApi control.
The data displayed can be sorted, but it is always based on a string sort.


This can result in unwanted sorting order. For instance, when a column is populated with numbers or dates, depending on
how the data is formatted, will display the data in the wrong sequence.


Have a look at the next screenshot. The number column and date column should be sorted in ascending order, but clearly the data is not
sorted on the number and date values we would like.


CListView DefaultSorting.png


This forces us to format the data in such a way they will be correctly sorted. For instance placing prefixed zero's on numbers or
a specific date format having year in front, then month and then the day.
But this is certainly not ideal and in most cases even unacceptable to the users.


There are two ways to remedy this:

  • Sort the listview on another column which is not visible but has the data formatted correctly. The so called "mirror" sorting
  • Implement the sorting routine yourself. So overrule the default listview string sorting and replace it with the sorting you like. So custom sorting


Mirror sorting


In this solution you populate the listview with extra columns having the needed formatted data. When sorted will result in the correct sequence.
So, the column which holds the wanted display formatting is not the column to sort on, but the sorting is done on another column.
Such a column should be invisible to the user.


One drawback: listviews do not support hiding of columns. On tables for example, we can implement hiding of certain columns and populate data we like.
Unseen by the user. Unfortunately, hiding of columns on listviews is not possible.


A common trick is to set the width of the column to zero (0). But that will not prevent the user to drag the column divider and resize the "hidden" column into view.
But this might be enough for your project to implement. A column having a width of zero pixels is invisible to the user.


In this solution we need to trap the click on the column header (the sort action), ignore the event so that the default sorting is not performed and execute the sort on the column we have "hidden".


The listview component sends WM_Notify messages to the control to inform specific actions. A notify structure is send along with it containing all info of the event.
We need to read the contents of this notify structure, detect that a column was clicked and, based on the column index, decide to ignore the sorting and perform the sort on another column.


The details how to do this can be found in the sample. One thing to know is that you have to return FALSE on the WM_Notify message to inform the control that sorting is not needed.
After this, call the listview sort on the column you want.


Custom sorting


The WinAPI listview control has a feature to be able to implement your own sort logic.
When you send the message LVM_SORTITEMSEX to the listview control, it will start a custom sort.
It does this using a callback function. The listview will call this callback function for each item in the listview.
The callback function will be called with two (2) item indexes. The function should return one of three (3) values:


  • Return a negative value (-1) if the first item should precede the second
  • Return a positive value (+1) if the first item should follow the second
  • Return zero (0) when the items are equal


So, based on the item index, you need to identify which data this represents. For instance, we should know that item on index 1 is a specific date.
The second item, on index 2, has its own date.


We should then compare the two dates and return -1, 0 or 1 depending on the date value. We are in charge how to do the compare.
The listview will continue calling the callback function for the items and knows in the end how to sort the column.


But, and now the unfortunate part, Team Developer does not support callback functions. A callback function is a pointer to a function, and TD does not offer this feature.
Fortunately, we have the great TD component, called Callback. Created by Christian Schubert. This handy DLL gives TD the callback feature we need and is available for all TD versions.


To register a callback function and get a pointer to be used in LVM_SORTITEMSEX is done using this:


Call SetCallback( "__CustomSortCompareFunc", 3, FALSE, nCallbackFunctionPtr, nCallbackProcessHandle )


What we are doing here is indicating which TD function acts like a callback function (the first parameter) and we will get back a function pointer.
In this case we pass in the function "__CustomSortCompareFunc". This is a real function which should be implemented in your code.
We get the function pointer back in the fourth (4) parameter, nCallbackFunctionPtr.


Having a function pointer, we can pass it as lParam when sending the LVM_SORTITEMSEX message to the listview control:


Call SalSendMsg( GetWindow( hWndItem, GW_CHILD ), LVM_SORTITEMSEX, 0, nCallbackFunctionPtr )


We have to send the message to the actual WinApi control and not the TD wrapper. To get the window handle of the WinApi control:


GetWindow( hWndItem, GW_CHILD )


(The WinApi control is the child of the cListView control)


The only thing to do now, is to implement the callback function itself. It should have the exact name you specified in the SetCallback function:


Function: __CustomSortCompareFunc
   Returns
      Number:
   Parameters
      Number: pnItemIndex1
      Number: pnItemIndex2
      Number: pnValue
   Actions
      ! Find out the real data of pnItemIndex1 and pnItemIndex2
      ! Then compare the real item data and return the needed value

      If item1 < item2
         Return -1
      Else If item1 > item2
         Return 1
      Else
         ! Items are equal
         Return 0


This function will automatically be called by the listview control until all items are compared.
After this, the column will be sorted using the sorting order you specified in the callback function.
The way you compare could be very complex, using multiple values from the real item. Any sort order for every kind of data can be implemented.


Sample application


To show both sorting methods, the mirror sorting and the custom sorting, a little sample has been created.
The archive contains the callback component for each TD version. So before starting the sample, be sure to copy the needed callback dll and library to the main folder of the sample.
Also uncomment the library include for callback in the libraries section.


When starting the sample you have this screen:


CListViewSortingSample.png


Initially the listview is sorted on the first column, "Number (default)". It is sorting using the default string sort method. The column is clearly not in the right order.


When clicking on the column headers, you can sort them:


  • The columns "Numbers (default)" and "Dates (default)" will sort the standard way
  • The columns "Numbers (custom)" and "Dates (custom)" will do a custom sort using the callback
  • The columns "Numbers (mirror)" and "Dates (mirror)" will do a mirror sort using the "hidden" extra columns


The checkbox "Show hidden columns" will toggle the mirrored columns widths so you can see how the formatted data looks like.


The buttons will execute a sort programmatically.
So the sample does not only support sorting when clicking on the column header, but you can execute a mirror and custom sort at any time in your code.


Here you can download the sample:
Down.png WIKI_cListView_CustomSorting.zip



Pointer2.png cListView: keep selected item highlighted Pointer.png

When selecting an item in cListView it is getting a bounded selection background color.
But when the listview is loosing focus, this selection color is changed to a less visible color. The item is still selected though.


See the image below which shows two listview objects.
Both listviews have an item selected. The datafield in the middle has the current focus.
The listview at the left has the default behavior. The selected item is not clearly visible.
The listview at the right shows a workaround to keep the item visually selected.


ListViewKeepItemFocus.png


The cListView object is a wrapper around the "SysListView32" WinApi control.
So how the items are displayed is managed by this default control and shows also in other Windows applications the issue
with the item selection color.


The workaround to keep the item visually selected even when the listview does not have the focus is this:


When the control receives WM_KILLFOCUS, it should return 0 (zero) to suppress the "unhighlighting" of the current selection.
It is not possible to implement this trick on listviews in TD without some extra work.
The cListView class is not the real listview object but a wrapper. The real object is a child of the cListView object.
So defining messages under the cListView object in the TD outline will not be triggered.
This means that we are unable to code the WM_KILLFOCUS message.


We can however implement it using SubClasser.
By subclassing the real listview control and fetch the WM_KILLFOCUS message, we can implement the message at cListView level.

On SAM_Create
   Call RegSubclass( GetWindow( hWndItem, GW_CHILD ), WM_KILLFOCUS, hWndItem, PAM_KillFocus, SUBCLASS_FLAG_INSTEAD )
On PAM_KillFocus
   !
   ! Selected item of the listview will still be highlighted when losing focus
   Return 0
On SAM_Destroy
	Call UnregSubclass( GetWindow( hWndItem, GW_CHILD ), WM_KILLFOCUS)


On create of the cListView object the WM_KILLFOCUS of the SysListView32 child object is subclassed so that it is send
to the cListView object as a custom message : PAM_KillFocus
Then this custom message is defined to return 0 (zero).
On destruction of the object, the subclassing is removed.


Here you can download a sample:
Down.png WIKI_ListView_KeepFocus.zip