<

Want to win a PS4? Go Premium and enter to win our High-Tech Treats giveaway. Enter to Win

x

List Control: Sorting Columns on a Header Click

Published on
19,722 Points
10,122 Views
1 Endorsement
Last Modified:
Awarded
Here's how to let your user change the sorting order of the rows in your ListView Control when he clicks on a header at the top of a column.  This is a U/I feature seen in all top applications, and one that you will surely want to support in your own programs.
Sort the list when the user clicks a column headerPopulating the List
Before we start with the sorting, let's look at a typical way to populate the list -- fill it with the data that the user will see (and, perhaps, just might want to sort).  

In the previous related article, List Control: Header Drag, Column Resize, and Remember User Settings, we didn't really discuss this -- any means you have to set the items and subitems will work.  For instance, you might cycle through a CRecordset-derived object and insert rows into the list as you go.  Another common technique is to provide an array of C structures and loop through that.  Here's how I did that for this article:
typedef struct {     // data layout for each row
    char* szName;
    char* szDate;
    char* szRef;
    char* szType;
    char* szStatus;
    char* szComment;
} SampleListData;

//------------------------ the data used in these examples
SampleListData arData[]= {
  {"Smith, Joe",  "2009-12-14", "A22222", "Std",   "Sent",  "This is a comment" },
  {"Smith, Alex", "2009-12-15", "A12346", "Std",   "Sent",  "This is a comment" },
  {"Jones, Jane", "2009-12-13", "A22223", "Jumbo", "Ready", "This is a comment" },
  {"Albert, Q.",  "2009-12-12", "A99999", "SP",    "Sent",  "This is a comment" },
  {0, 0, 0, 0, 0, 0 }, // signal end of list
};

Open in new window

And here's the loop that shows how these data items are put into the list:
//--------- uses the CListCtrlEx.PutItem() function from previous article
void CListClmsDlg::PopulateList() {
    SampleListData* pr= arData;
    int nListIdx= 0;
    while( pr->szName ) {
        m_ctlList.PutItem( nListIdx, 0, pr->szName    , TRUE);
        m_ctlList.PutItem( nListIdx, 1, pr->szDate    );
        m_ctlList.PutItem( nListIdx, 2, pr->szRef     );
        m_ctlList.PutItem( nListIdx, 3, pr->szType    );
        m_ctlList.PutItem( nListIdx, 4, pr->szStatus  );
        m_ctlList.PutItem( nListIdx, 5, pr->szComment );

        m_ctlList.SetItemData( nListIdx, (DWORD)pr ); // << NEEDED 
        nListIdx++;
        pr++;
    }
}

Open in new window

An important part of this is that at line 13, after inserting a new row and populating the subitems, the function also saves a pointer to the original data (a SampleListData* value) as the item data.  This is a key to providing the ability to sort the list on the fly.

Simple Example
Let's start with a minimal example.  We'll set up so that the parent window -- typically a dialog box -- will get a notification of a click on a header in the ListView control, and we'll do do all of the sorting in the CDialog-derived object.  We'll assume that you already have a dialog box containing a List Control with a resource ID of IDC_LIST1.

1) In the Dialog Editor, right-click the top of the dialog and select Properties.
2) In the Properties window, click the "Control Events" button (it looks like a lightening bolt) and open up IDC_LIST1.  
3) Click on the LVN_COLUMNCLICK event and choose <Add> OnLvnColumnClickList1

This puts you in the source code editor at the new message handler,  OnLvColumnclickList1(), in your parent dialog.  For this first example, we're going to illustrate the sorting mechanism in the simplest possible terms.  We'll treat all header clicks as a request to sort the first column,  And we'll always sort A-Z (ascending).   Here's the code for that:
int CALLBACK MyCompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
{
    SampleListData* pr1= (SampleListData*) lParam1;
    SampleListData* pr2= (SampleListData*) lParam2;
    CString s1= pr1->szName;
    CString s2= pr2->szName;

    int nRet= 0;              // indicates they are equal
    if ( s1 > s2 ) nRet=  1; // item 1 should come after  item 2
    if ( s1 < s2 ) nRet= -1; // item 1 should come before item 2
    return( nRet );
}

void CListClmsDlg::OnLvnColumnclickList1(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
    m_ctlList.SortItems( MyCompareFunc, 0 );
    *pResult = 0;
}

Open in new window

This requires some explanation.  When you invoke the CListCtrl.SortItems() function, you pass to it the address of a comparison function.  That comparison function will operate on the item data that is associated with two different rows in the list control.  That's why we used...
   m_ctlList.SetItemData( nListIdx, (DWORD)pr ); // << NEEDED for sorting

Open in new window

...in the PopulateList() function.

Now the comparison callback function will receive two pointers to the original data so that it can decide which item should be put higher in the list.

Toggling the Sort Order
Next... Your users will expect the "usual behavior" of being able to click a second time to sort in the opposite order.  Let's look at that before we move on to adding the logic to sort on any column.  Again, we'll use simplified logic for the demo.  

Let's assume that the list starts out in random order.  The first click on the header sorts ascending (A-Z) and the second click sorts descending (Z-A).   We'll create an enumeration that includes these three possibilities and a global variable that the callback can see.  Then the OnClickColumn handler can toggle the setting and the callback can determine how to compare.

typedef enum {
    SORT_None = 0,
    SORT_AZ   = 1,
    SORT_ZA   = -1,
} SortOrder;

SortOrder geOrder= SORT_None; // global variable, for now

void CListClmsDlg::OnLvnColumnclickList1(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);

    if ( geOrder == SORT_None ) geOrder= SORT_AZ;
    else geOrder= (SortOrder)-geOrder; // -1 to 1, 1 to -1

    m_ctlList.SortItems( MyCompareFunc, 0 );
    *pResult = 0;
}

Open in new window

Notice that the SortOrder enumerated data type is set up so that it can toggle by reversing the sign (-1 becomes 1 and vice versa).  The callback function now looks like this:
int CALLBACK MyCompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
{
    SampleListData* pr1= (SampleListData*) lParam1;
    SampleListData* pr2= (SampleListData*) lParam2;

    if ( geOrder == SORT_ZA ) { // sort descending, just swap
        pr2= (SampleListData*) lParam1;
        pr1= (SampleListData*) lParam2;
    }
    CString s1= pr1->szName;
    CString s2= pr2->szName;
    int nRet= 0;
    if ( s1 > s2 ) nRet=  1;
    if ( s1 < s2 ) nRet= -1;
    return( nRet );
}

Open in new window

I've found that the best way to reverse the sort order is to just swap the parameters right at the start (lines 6-9).  It's easier than an if...then for each case (which can get confusing... "Am I going up or down? If down, do I use < or > ?" etc.)

Sorting By Any Column
Up until now, we've been sorting on the first column ("Name") regardless of which header was clicked.  Now we have to complicate things by adding logic to sort by any column, but it's not all that hard if you've followed to this point.  

The OnColumnClick handler gets a parameter that lets you know which column header was clicked.  So all we need to do is save that in a global variable (gnSortClm) so the callback can take special action depending on the column.  Here's some code that does that.
SortOrder geOrder=  SORT_None;
int       gnSortClm= -1;   // not set

void CListClmsDlg::OnLvnColumnclickList1(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);

    if ( geOrder== SORT_None) geOrder= SORT_AZ;
    else geOrder = (SortOrder) -geOrder; 

    gnSortClm= pNMLV->iSubItem; // <<<<< save column# for the callback

    m_ctlList.SortItems( MyCompareFunc, 0 );
    *pResult = 0;
}

Open in new window

...and here's the callback:
int CALLBACK MyCompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
{
    SampleListData* pr1= (SampleListData*) lParam1;
    SampleListData* pr2= (SampleListData*) lParam2;
    if ( geOrder == SORT_ZA ) { // sort descending, just swap
        pr2= (SampleListData*) lParam1;
        pr1= (SampleListData*) lParam2;
    }
    CString s1, s2;
    switch( gnSortClm ) {
        default: s1= pr1->szName;   s2=pr2->szName;    break; // clm 0
        case  1: s1=pr1->szDate;    s2=pr2->szDate;    break;
        case  2: s1=pr1->szRef;     s2=pr2->szRef;     break;
        case  3: s1=pr1->szType;    s2=pr2->szType;    break;
        case  4: s1=pr1->szStatus;  s2=pr2->szStatus;  break;
        case  5: s1=pr1->szComment; s2=pr2->szComment; break;
    }
    int nRet= 0;
    if ( s1 > s2 ) nRet=  1;
    if ( s1 < s2 ) nRet= -1;
    return( nRet );
}

Open in new window

In case you are wondering... What if the user has rearranged the columns, as described in the previous article?  Well, the answer is... Yes, it works perfectly.  The LPNMLISTVIEW that comes into the OnColumnClick handler provides the original column index -- even if the user has shuffled them and even if you have restored the arrangement from a previous session.

More Options
Here are some directions you can go to modify these examples for your own programs:

If the column data is numeric, you'll find that sorting on a textual representation of that value may not work as expected.  For instance, a value of "20" sorts above (less than) a value of "9".  In that case, you should extract the two binary values and compare them:
...(in the callback)... ... if ( gnClmIdx==3 ) { // or whatever (numeric data) int n1= atoi( pr1->szNumval ); int n2= atoi( pr2->szNumval ); return( n1-n2 ); // negative if n2 is greater than n1 }And you can use similar special-case handling for dates, or any other kind of data.  For instance, the textual subitems in a "Status" column might be one of Waiting, Processing, and Done and you might want to sort on the real underlying values (say, 1, 2, and 3), so that a re-ordering puts them into an internal logical order, rather than an alphabetical order.
Sorting by date is a special case.  Different locales show dates as d/m/y or m/d/y.  My solution is to ignore locale and display all dates as yyyy-mm-dd -- which has the inherent advantage that an alphabetical sort also sorts by date.  If that is not to your liking, you can write special logic to convert the date to a numeric value (say, days since 1900-01-01) and sort on that.  Another option is to maintain a hidden column with the date in a sort-friendly format, and sort on the value in that column.

Recall that the comparison function is actually provided with item data values, which point into a structure array that we maintain.  In the example, these structure fields are all character strings, but it's quite common for them to contain dates, binary numeric values, and perhaps enumerated datatype values.  You need not be constrained by textual sorting -- you have access to the underlying data when you decide what comes first.
We used global variables to communicate with the callback.  But notice that there is a parameter in the CListCtrl::SortItems() function that we did not use.  You could pass a pointer to a structure that contains the two needed items (column and sort order) to avoid using global variables.
You may not have noticed, but in the code that toggles the sort order we had just one global variable, geSortOrder, which we toggle each time.  If you have just sorted the Name column ascending, then if the user clicks the Date column, it will sort on that column in descending order.  You really need to keep a "last used sort order" for each column.  In the previous article, the column-definition structure provided a place to store the current sort order.  Use something like that to avoid hearing people say that your program has a bug.
We used a global callback function, but it is possible to make the callback function a member variable of the containing object (in this case the dialog).  Just use a handy technique described here: How to provide a CALLBACK function into a C++ class object and pass the dialog's this pointer to the callback.
 
In one project, I found that the ListView control was too sluggish in sorting an extra-large collection of data.  In that case, I completely ignored the ClistCtrl::SortItems() feature, and instead, used a quicksort (see qsort) to re-order the data, then I repopulated the list.

When working with very large datasets obtained from a database, there is yet another option:  Rather than sorting the data locally, you can requery the recordset and specify a different ORDER BY clause in your SELECT statement.  You my find that even with the communication overhead and the fact that you must completely repopulate the list, this may be a more efficient sorting method.
In some programs, you can press the SHIFT key while clicking a header to force an Z-A (descending) sort without having to sort twice.   It's pretty easy to do that.  In the OnLvnColumnClick function, you can use something like:
if ( ::GetKeyState(VK_SHIFT) & 0x8000 ) { geOrder= SORT_ZA; }

References:

List Control: Header Drag, Column Resize, and Remember User Settings

How to provide a CALLBACK function into a C++ class object

CListCtrl (MFC class)
http://msdn.microsoft.com/en-us/library/hfshke78(VS.80).aspx

List View (Win API Common Controls)
http://msdn.microsoft.com/en-us/library/bb774737(VS.85).aspx

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
If you liked this article and want to see more from this author,  please click the Yes button near the:
      Was this article helpful?
label that is just below and to the right of this text.   Thanks!
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
1
Comment
Author:DanRollins
[X]
Welcome to Experts Exchange

Add your voice to the tech community where 5M+ people just like you are talking about what matters.

  • Help others & share knowledge
  • Earn cash & points
  • Learn & ask questions
0 Comments

Featured Post

New feature and membership benefit!

New feature! Upgrade and increase expert visibility of your issues with Priority Questions.

Join & Write a Comment

This is Part 3 in a 3-part series on Experts Exchange to discuss error handling in VBA code written for Excel. Part 1 of this series discussed basic error handling code using VBA. http://www.experts-exchange.com/videos/1478/Excel-Error-Handlin…
In this video, Percona Solution Engineer Dimitri Vanoverbeke discusses why you want to use at least three nodes in a database cluster. To discuss how Percona Consulting can help with your design and architecture needs for your database and infras…

Keep in touch with Experts Exchange

Tech news and trends delivered to your inbox every month