A guide to sorting

Sorting Control Values

There are three controls that have implicit sorting: CListBox, ComboBox, and CListCtrl. Each of these has a way of specifying a sort.

CListBox and CComboBox

These controls are basically the same. The downside is that you need to make them owner-draw controls without strings. This is because the message WM_COMPAREITEM is not sent to the parent window unless the control is both owner-draw and does not have strings. In MFC, you do not handle this in the parent window! Instead, you use the virtual method CompareItem. You use the ClassWizard or the events property (in VS7) to specify a CompareItem method. The CompareItem method gets a pointer to a COMPAREITEMSTRUCT:

typedef struct tagCOMPAREITEMSTRUCT {
  UINT CtlType;
  UINT CtlID;
  HWND hwndItem;
  UINT itemID1;
  ULONG_PTR itemData1;
  UINT itemID2;
  ULONG_PTR itemData2;
  DWORD dwLocaleId;
} COMPAREITEMSTRUCT;

The normal template created when you add a CompareItem method is

int CompareItem(LPCOMPAREITEMSTRUCT lpCompareItemStruct)
{
    return 0;
}

The first thing I do is replace that ridiculous parameter name by something sensible:

int CompareItem(LPCOMPAREITEMSTRUCT cis)
{
    return 0;
}

You can then treat the itemData1 and itemData2 pointers as if they were the inputs to the compare routine we discussed for qsort. There is, however, a more serious problem. Where qsort could accept any negative value for item1 less than item2, and any positive value for item1 greater than item2, Windows requires very specific values: -1, 0, and 1. So the code looks a lot like the COleDateTime example. You create these methods in a subclass of the control you wish to implement.

In addition, this really only applies to combo boxes with the "drop list" style. You can't have an owner-drawn combo box with an edit control, but without the Has Strings option.

int CMyListBox::CompareItem(LPCOMPAREITEMSTRUCT cis)
{
    pmyData data1 = (pmyData)cis->itemData1;
    pmyData data2 = (pmydata)cis->itemData2;
    if(data1->year < data2->year)
    return -1;
    if(data1->year > data2->year)
    return 1;
    int result = _tcscmp(data1->name, data2->name);
    if(result < 0)
    return -1;
    if(result > 0)
    return 1;
    return 0;
}

If you only need a simple CListBox or CCombBox, the DrawItem code is quite simple. The string that is printed is based on your own transformation of the contents of your data structure. While it makes sense to print it out the fields in the order of the sort keys, I've done various kinds of decorations as well. For example, using color highlights, putting punctuation marks in, displaying a flag, and so on. And while I do it here as a single TextOut, you can columnize it nicely, or do whatever you want. I have highlighted the actual output code below; the rest is infrastructure. Note that I have also replaced the absurd name lpDrawItemStruct for the parameter with something much more sensible: dis.

void CInstrumentSelector::DrawItem(LPDRAWITEMSTRUCT dis)
{
    CRect r = dis->rcItem;
    CDC * dc = CDC::FromHandle(dis->hDC);
    COLORREF txcolor;
    COLORREF bkcolor;
    int saved = dc->SaveDC();
    if(dis->itemState & ODS_SELECTED)
    { /* selected */
        bkcolor = GetSysColor(COLOR_HIGHLIGHT);
        txcolor = GetSysColor(COLOR_HIGHLIGHTTEXT);
    } /* selected */
    else
    { /* unselected */
        if(dis->itemState & (ODS_DISABLED | ODS_GRAYED))
        txcolor = GetSysColor(COLOR_GRAYTEXT);
        else
        txcolor = GetSysColor(COLOR_WINDOWTEXT);
        bkcolor = GetSysColor(COLOR_WINDOW);
    } /* unselected */
    dc->SetBkColor(bkcolor);
    dc->SetTextColor(txcolor);
    dc->FillSolidRect(&r, bkcolor);
    //****************************************************************
    // Print the data out in the format
    // yyyy name...
    pmyData data = (pmyData)dis->itemData;
    CString s;
    s.Format(_T("%04d %s"), data->birthday, data->name);
    //****************************************************************
    if(dis->itemID != (UINT)-1
    && (dis->itemState & (ODS_DISABLED | ODS_GRAYED)) == 0)
    { /* has item */
    dc->TextOut(r.left, r.top, s);
    } /* has item */
    if(dis->itemState & ODS_FOCUS && dis->itemAction != ODA_SELECT)
    dc->DrawFocusRect(&r);
    dc->RestoreDC(saved);
}


The CListCtrl

The CListCtrl is very peculiar. Each element in a CListCtrl has an LPARAM value, which you can set explicitly as part of the structure you pass in, or by using the SetItemData method. When you invoke a sort request, you provide a sort routine. The sort routine is not provided with the index of the items to compare, but simply the LPARAM values.

Therefore, if you want a sortable CListCtrl, you must explicitly set the LPARAM field of each entry to point to, or contain, something useful for doing a comparison. A common technique is to put a pointer to an object in this field. For example, if I want to add the information to a CListCtrl, I might add the year as the first column (converted to a string representation), and the name in the second column, but I also set a pointer to the data object as the ItemData value. In the sort routine, I then have access to this.

void CMyListControl::sortbyageandname()
{
    SortItems(compareByAgeAndName, NULL);
}

The compare function must be declared as a static method, e.g.,

class CMyListControl : public CListCtrl {
...
protected:
    static int compareByAgeAndName(LPARAM v1, LPARAM v2, LPARAM parm);
};
/* static */ int CMyListControl::compareByAgeAndName(LPARAM v1, LPARAM v2, LPARAM)
{
    pmyData data1 = (pmyData)v1;
    pmyData data2 = (pmyData)v2;
    int result = (int)data1->birthday - (int)data2->birthday;
    if(result != 0)
    return result;
    return _tcscmp(data1->name, data2->name);
}

Note that unlike the CListBox and CComboBox, simple negative and positive values will function correctly. The specific values -1 and 1 are not required.

Suppose I want to have multiple sorts. For example, by clicking in a header control, detecting the desired column. What I do is respond to the button and set a "desired sort" field in my control. But I need to determine in the comparison routine which sort to do. But the sort function must be static, which means it can't access any class variables!

Now a naive (and incorrect) solution to this problem might be to store the desired sort in a static variable, which would make it accessible to the static method. This is so unbelievably incorrect that it is hard to imagine anyone would want to do it. Imagine if you had two controls! Which one would control the sorting? Whichever one the user selected most recently! It would set the sort style for both! This, of course, would be completely nonsensical.

There are two ways to handle this. The simplest one is to pass that variable in as the user-defined parameter. For example

class CMyListControl : public CListCtrl {
    ...
public:
    typedef enum {SORT_BY_BIRTHDAY, SORT_BY_NAME} SortType;
    void SortByWhatever(SortType t);
protected:
    static int compareWhatever(LPARAM v1, LPARAM v2, LPARAM parm);
    ...
};

Then to invoke the sort, you would have, in your dialog

class CMyDialog : public CDialog {
    protected:
    CMyListControl::SortType DesiredSort;
    void SortByWhatever();
};
void CMyDialog::SortByWhatever()
{
    c_List.SortByWhatever(DesiredSort);
}
void CMyListControl::SortByWhatever(SortType type)
{
    SortItems(compareWhatever, (DWORD_PTR)type);
}
/* static */ int CMyListControl::compareWhatever(LPARAM v1, LPARAM v2, LPARAM param)
{
    pmyData data1 = (pmyData)v1;
    pmyData data2 = (pmyData)v2;
    switch((SortType)param)
    { /* SortType */
        case SORT_BY_BIRTHDAY:
        { /* birthday */
        int result = (int)data1->birthday - (int)data2->birthday;
        if(result != 0)
            return result;
        return _tcscmp(data1->name, data2->name);
        } /* birthday */
        case SORT_BY_NAME:
        return _tcscmp(data1->name, data2->name);
        default:
        ASSERT(FALSE); // unknown sort type
        return 0; // treat as same
    } /* SortType */
}

The second method is to pass a pointer to the class and access the class member variable by casting this pointer to be the class pointer type. This seems to expose more of the class than is necessary, and is not a recommended method.

You might also like...

Comments

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“Before software should be reusable, it should be usable.” - Ralph Johnson