CSortedListCtrl reusable base class

Download the source and example.

I found several articles about sorting a CListCtrl, adding visual feedback
in the header… on the codeguru page and I have encapsulated this into a
reusable base class CSortedListCtrl that encapsulates code from several
other articles. With this class, the user doesn’t have to care about all
this sorting stuff, header control, sorting callback… He just has to
override some virtual routines to implement its sorting algorithm. The
Zip file contains a Word 97 DOC for the documentation (ed. which is duplicated here), the
SortedListCtrl project encapsulating all the code from previous articles and
a project (TestListCtrl) showing how to use the CSortedListCtrl class.

It has been compiled with Visual Studio 97.

I hope this will be useful to other programmers just like the articles on
the CodeGuru page were for me.


Introduction

This is a reusable CSortedListCtrl class based on several articles
from the ListView Control section of CodeGuru :

  • “Indicating sort order in header control – Zafir Anjum”
  • “Sorting the list when user clicks on column header – Zafir Anjum”
  • “Vertical lines for column borders – Zafir Anjum”

How to use the class

A compilable test project is available and I will only use some
extracts here to explain the code. There is no explanation on the code
from the other articles, just go there and read them if you are
interested.

I have put all the CSortedListCtrl stuff in a separate library file so
I can use it from all my projects. To use the class, you need the
“SortedListCtrl” project which will produce a library. Insert this
project into your project (in the sample, I have used a Dialog based
app called TestListCtrl) that will be using the CSortedListCtrl and
set a dependency so it will recompile automatically.

CSortedListCtrl acts as a base class like CDialog. So you will need to
derive your own class : let’s say CMyListCtrl. You can make it using
Class Wizard : you drop a CListCtrl on your dialog box, set it in
report mode and add a class CMyListCtrl setting CListCtrl as a base
class but you will have to change CListCtrl into CSortedListCtrl in
Wizard generated code (in .h and message map) You will also need to
#include “SortedListCtrl.h”, add an additional include directory
(the SortedListCtrl directory) in the project settings, define a
variable for your list control (m_ListCtrl in the example) and set its
type to CMyListCtrl (not CListCtrl).

I use a ListCtrl with two columns and no image. Let’s say a column
Name of type string and a column Number of type integer to illustrate
the sorting routine with two different types. You need to build the
list columns in the OnInitDialog routine. To clearly show the code
added, this is done in an ExtraInit function called from OnInitDialog
:


void CTestListCtrlDlg::ExtraInit()
{
	CRect Rect1;
	m_ListCtrl.GetWindowRect(Rect1);
	int cx = (Rect1.Width() - 4) >> 1;

	m_ListCtrl.InsertColumn(0, "Name", LVCFMT_LEFT, cx);
	m_ListCtrl.InsertColumn(1, "Number", LVCFMT_LEFT, cx);

	FillList();

	// Add full row selection to listctrl
	m_ListCtrl.SetFullRowSel(TRUE);

	// Sort the list according to the Name column
            m_ListCtrl.SortColumn(0, TRUE);
}

SetFullRowSel is a method of CSortedListCtrl that allows to select
the entire line, not only the first column while the SortColumn method
allows you to sort the list on a given column and choosing ascending
or descending order (sorting mechanism is explained later).

Now let’s have a look at the FillList routine :


void CTestListCtrlDlg::FillList()
{
    CMyItemInfo *pItemInfo;
     int iItem = 0;
    CString Name;
    int Number;

    Name = "Sharon Stone";
    Number = -1;

    m_ListCtrl.InsertItem(iItem, LPSTR_TEXTCALLBACK);
    pItemInfo = new CMyItemInfo (iItem, Name, Number);
    m_ListCtrl.SetItemData(iItem, (DWORD)pItemInfo);

    m_ListCtrl.SetItemText(iItem, 1, LPSTR_TEXTCALLBACK);

    iItem++;

    Name = "Julia Roberts";
    Number = 1;

    m_ListCtrl.InsertItem(iItem, LPSTR_TEXTCALLBACK);
    pItemInfo = new CMyItemInfo (iItem, Name, Number);
    m_ListCtrl.SetItemData(iItem, (DWORD)pItemInfo);

    m_ListCtrl.SetItemText(iItem, 1, LPSTR_TEXTCALLBACK);
}

It just fills the list with two items but won’t compile yet
because it needs another class used by the sorting mechanism. Sorting
the list using the standard mechanism provided by CListCtrl requires
that you attach some item data to your items because what will be
given to you in the compare routine (see later) will be pointers to
these item data. In order to hide the callback mechanism used by
CListCtrl and be able to use polymorphism, the data attached to the
item is encapsulated in a class. I give you a base class CitemInfo
from which you can derive your own class, let’s say CMyItemInfo. You
can do this with class wizard, specifying a generic class deriving
publicly from CItemInfo (ItemInfo.h is provided in the SortedListCtrl
project). You will have to do some #includes with ItemInfo.h and
MyItemInfo.h.

Now, you can just add some class members, constructor and methods to
your MyItemInfo class to set and retrieve easily information. To make
it easy, I have put everything in the MyItemInfo.h file :


#include "ItemInfo.h"

class CMyItemInfo : public CItemInfo
{
public:

    CMyItemInfo(int iItem, CString& Name, int Number) :
		CItemInfo(iItem),
		m_Name(Name),
		m_Number(Number) {m_NumberAsString.Format("%d",Number); };

    virtual ~CMyItemInfo() {};

    CString& GetName() {return m_Name;}
    int GetNumber() {return m_Number;}
    CString& GetNumberAsString() {return m_NumberAsString;}

private:

    CMyItemInfo();

    CString m_Name;
    CString m_NumberAsString;
    int m_Number;

};


The basic idea for the sort to work is to put the data inside the
MyItemInfo class which will be passed to the comparison routine. In
order not to duplicate data (it’s a time/space compromise choice
here), the LPSTR_TEXTCALLBACK is used. To make it work, you must add
the following line in MyListCtrl.h :


afx_msg void OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult);

and the following one in the message map of MyListCtrl.cpp :


ON_NOTIFY_REFLECT(LVN_GETDISPINFO, OnGetDispInfo)

Then, you have to implement the OnGetDispInfo routine :


void CMyListCtrl::OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult)
{
	LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;

	if (pDispInfo->item.mask & LVIF_TEXT) {

		CMyItemInfo* pAppItem = reinterpret_cast(pDispInfo->item.lParam);

		switch (pDispInfo->item.iSubItem) {

		case 0 :
			lstrcpy (pDispInfo->item.pszText, pAppItem->GetName());
			break;
		case 1 :
			lstrcpy (pDispInfo->item.pszText, pAppItem->GetNumberAsString());
			break;
		}
	}
	*pResult = 0;
}

It’s not finished yet ! You have to give a
correct constructor for the CMyListCtrl class :


CMyListCtrl::CMyListCtrl() : CSortedListCtrl(TRUE, TRUE)
{
}

The first constructor parameter tells the base class if you want
to sort the column (it’s normally what you want here !) and the second
parameter informs the base class to handle the deletion of the item
data itself. In fact, if you remember the code in FillList, we
allocate objects of type CMyItemInfo on the heap to attach to the list
items. When elements of the list are deleted, these MyItemInfo must be
also deleted and the base class can do it for you. This is possible
because CMyItemInfo derives from ItemInfo and the CItemInfo destructor
is virtual.

Now, we must still provide a comparison routine : this is done by
overridding a virtual function from CSortedListCtrl that is used to
sort the elements in the list. This is just an OO translation of the
callback mechanism used by the standard control that’s now hidden in
the base class.

Just add the following method declaration in your MyListCtrl.h file :


int CompareItems(CItemInfo *pItemInfo1, CItemInfo *pItemInfo2);

and the following definition in the MyListCtrl.cpp file :


int CMyListCtrl::CompareItems(CItemInfo *pItemInfo1, CItemInfo *pItemInfo2)
{
	CMyItemInfo *pInfo1 = static_cast(pItemInfo1);
	CMyItemInfo *pInfo2 = static_cast(pItemInfo2);
	int nResult;

	switch (GetSortedColumn())
	{
	case 0 : // Sort on column 'Name'
		nResult = pInfo1->GetName().CompareNoCase(pInfo2->GetName());
		break;
	case 1 : // Sort on column 'Number'
	{
		int Number1 = pInfo1->GetNumber();
		int Number2 = pInfo2->GetNumber();

		if (Number1 < Number2)
		{
			nResult = -1;
		}
		else
		{
			nResult = (Number1 != Number2);
		}
		break;
	}
	default :
		nResult = 0;
		break;
	}
	return IsAscSorted() ? nResult : -nResult;
}

In fact, this function will be called for each pair of item in the
list that must be compared and you receive as parameters, the item
data attached to each of the element (I will describe below how to
attach these ItemInfo to the list items). In this function, you are
likely to use two functions from the base class : GetSortedColumn and
IsAscSorted which are telling you respectively the column to be sorted
and the sort order (ascending or descending). The return value has the
same semantic as the callback routine from CListCtrl or the strcmp
functions (-1, 0 or 1). Note also the safe static_cast here because
CMyItemInfo inherits from CItemInfo.

That’s all folks !

Last updated: 6 June 1998

More by Author

Must Read