csortinglistviewwinapi

WinAPI - How to implement ListView sorting?


Trying to sort values in a WinAPI ListView in C, with code based on examples, MSDN and multiple Q&A forums including SO. The code to create the columns and insert the items is as follows:

int CreateColumn(HWND hwndList, int col_number, wchar_t* title, int width)
{
    LVCOLUMN lvc;
    lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
    lvc.fmt = LVCFMT_LEFT;
    lvc.cx = width;
    lvc.pszText = title;
    lvc.iSubItem = col_number;

    return ListView_InsertColumn(hwndList, col_number, &lvc);
}

void InsertItem(HWND hwndList, int row, wchar_t* txt0, wchar_t* txt1, wchar_t* txt2)
{
    LVITEM lvi = { 0 };

    lvi.mask = LVIF_TEXT | LVIF_PARAM;
    lvi.iItem = row;

    lvi.iSubItem = 0;
    lvi.lParam = (LPARAM)txt0;
    lvi.pszText = txt0;
    ListView_InsertItem(hwndList, &lvi);
    ListView_SetItemText(hwndList, row, 0, txt0);

    lvi.iSubItem = 1;
    lvi.lParam = (LPARAM)txt1;
    lvi.pszText = txt1;
    ListView_SetItem(hwndList, &lvi);
    ListView_SetItemText(hwndList, row, 1, txt1);

    lvi.iSubItem = 2;
    lvi.lParam = (LPARAM)txt2;
    lvi.pszText = txt2;
    ListView_SetItem(hwndList, &lvi);
    ListView_SetItemText(hwndList, row, 2, txt2);
}

// in another function

CreateColumn(hWndListView, 0, L"Col 0", 150);
CreateColumn(hWndListView, 1, L"Col 1", 150);
CreateColumn(hWndListView, 2, L"Col 2", 150);

InsertItem(hWndListView, 0, L"000.0", L"000.9", L"000.10");
InsertItem(hWndListView, 1, L"000.1", L"000.8", L"000.30");
InsertItem(hWndListView, 2, L"000.2", L"000.7", L"000.20");

Then the comparer function and the event handler are the following:

case WM_NOTIFY:
    switch (((LPNMHDR)lParam)->code)
    {
    case LVN_COLUMNCLICK:
        {
            OnColumnClick((LPNMLISTVIEW)lParam);
            break;
        }
    }
    break;

int CALLBACK myCompFunc(LPARAM lp1, LPARAM lp2, LPARAM sortParam)
{
    BOOL isAsc = (sortParam > 0);
    int column = abs(sortParam) - 1;

    wchar_t *p1, *p2;
    p1 = (wchar_t*)lp1;
    p2 = (wchar_t*)lp2;

    if (isAsc)
        return strcmp(p1, p2);
    else
        return strcmp(p2, p1);
}

void OnColumnClick(LPNMLISTVIEW pLVInfo)
{
    static int nSortColumn = 0;
    static BOOL bSortAscending = TRUE;
    LPARAM lParamSort;

    // get new sort parameters
    if (pLVInfo->iSubItem == nSortColumn)
        bSortAscending = !bSortAscending;
    else
    {
        nSortColumn = pLVInfo->iSubItem;
        bSortAscending = TRUE;
    }

    // combine sort info into a single value we can send to our sort function
    lParamSort = 1 + nSortColumn;
    if (!bSortAscending)
        lParamSort = -lParamSort;

    // sort list
    ListView_SortItems(pLVInfo->hdr.hwndFrom, myCompFunc, lParamSort);
}

I noticed that if instead of hardcoded text I pass pointers to the InsertItem function, then clicking on a column has an effect (it "kinda" sorts, not exactly as expected, but the order of the items change). If there is only the hardcoded text, clicking on the headers has no effect.

Question 1: Why does this not work with hardcoded values?

Question 2: Is there a "better" function to compare strings in C than strcmp? Is this the reason why sorting does not give expected results (incorrect sorting, ex. 10 -> 30 -> 20 is never sorted)?

Edit In the debugger (VS2019 on Win10), values in the comparer function seem badly encoded. Somehow looks like parameters types are lost somewhere.


Solution

  • lvi.lParam can be an integer value, or a pointer. If it's a pointer then it usually points to data allocated in heap.

    Moreover, each row in listview can be associated with only one lvi.lParam. It seems you are attempting to associate separate lvi.lParam data for each column in each row. But this can't be done, so the second call to ListView_SetItem will fail because LVIF_PARAM is set, and lvi.iSubItem is greater than zero. This code will have no effect:

    lvi.mask = LVIF_TEXT | LVIF_PARAM;
    lvi.iItem = row;
    lvi.iSubItem = 1;
    lvi.lParam = (LPARAM)txt1;
    lvi.pszText = txt1;
    ListView_SetItem(hwndList, &lvi); //<== this call will fail
    


    Solution 1: using ListView_SortItemsEx:

    We ignore lParam, we use ListView_GetItemText to get the text in compare function:

    typedef struct
    {
        HWND hlist;
        int  iSubItem;
        BOOL bSortAscending;
    }t_data;
    
    int CALLBACK myCompFuncEx(LPARAM lp1, LPARAM lp2, LPARAM sortParam)
    {
        t_data *data = (t_data*)sortParam;
        wchar_t buf1[100], buf2[100];
        ListView_GetItemText(data->hlist, lp1, data->iSubItem, buf1, _countof(buf1));
        ListView_GetItemText(data->hlist, lp2, data->iSubItem, buf2, _countof(buf2));
        int res = wcscmp(buf1, buf2);
        return data->bSortAscending ? res >= 0 : res <= 0;
    }
    
    void OnColumnClickEx(LPNMLISTVIEW pLVInfo)
    {
        static int nSortColumn = 0;
        static BOOL bSortAscending = TRUE;
        if(pLVInfo->iSubItem != nSortColumn)
            bSortAscending = TRUE;
        else
            bSortAscending = !bSortAscending;
        nSortColumn = pLVInfo->iSubItem;
    
        t_data data;
        data.hlist = pLVInfo->hdr.hwndFrom;
        data.iSubItem = nSortColumn;
        data.bSortAscending = bSortAscending;
        ListView_SortItemsEx(pLVInfo->hdr.hwndFrom, myCompFuncEx, &data);
    }
    


    Solution 2: using ListView_SortItems:

    We can allocate separate memory for the strings, then associate lParam value to that string allocation.

    This method is more complicated, but it can be faster because we are making N calls to ListView_GetItemText (where N is the total count in ListView). In previous solution we make N * log(N) calls ListView_GetItemText, there might be a noticeable difference if the list is too large (though I haven't tested it)

    void OnColumnClick(LPNMLISTVIEW pLVInfo)
    {
        static int column = 0;
        static BOOL ascending = TRUE;
        if(pLVInfo->iSubItem != column)
            ascending = TRUE;
        else
            ascending = !ascending;
        column = pLVInfo->iSubItem;
    
        HWND hlist = pLVInfo->hdr.hwndFrom;
        int count = ListView_GetItemCount(hlist);
        if(count < 1) return;
    
        //allocate strings
        wchar_t** arr = malloc(count * sizeof(wchar_t*));
        LVITEM lvi = { 0 };
        lvi.mask = LVIF_PARAM;
        for(int i = 0; i < count; i++)
        {
            //get column string
            wchar_t buf[100]; //random max buffer size, hopefully it's big enough
            ListView_GetItemText(hlist, i, column, buf, _countof(buf));
            arr[i] = _wcsdup(buf);
    
            //match lParam to the string
            lvi.lParam = (LPARAM)arr[i];
            lvi.iItem = i;
            ListView_SetItem(hlist, &lvi);
        }
    
        ListView_SortItems(hlist, myCompFunc, (LPARAM)&ascending);
    
        //cleanup
        for(int i = 0; i < count; i++)
            free(arr[i]);
        free(arr);
    }
    
    int CALLBACK myCompFunc(LPARAM lp1, LPARAM lp2, LPARAM sort_data)
    {
        BOOL ascending = *(BOOL*)(sort_data);
        int res = wcscmp((const wchar_t*)lp1, (const wchar_t*)lp2);
        return ascending ? res >= 0 : res <= 0;
    }
    

    Edit, we can change InsertItem function so that ListView_InsertItem to return an index, then use that index in ListView_SetItemText.

    This will be safer in general. For example, if LVS_SORTASCENDING/LVS_SORTDESCENDING is set, then the index returned by ListView_InsertItem is not same as the row that was requested.

    void InsertItem(HWND hwndList, 
        const wchar_t* txt0, const wchar_t* txt1, const wchar_t* txt2)
    {
        LVITEM lvi = { 0 };
        lvi.mask = LVIF_TEXT;
        lvi.pszText = (wchar_t*)txt0;
        int index = ListView_InsertItem(hwndList, &lvi);
        ListView_SetItemText(hwndList, index, 1, (wchar_t*)txt1);
        ListView_SetItemText(hwndList, index, 2, (wchar_t*)txt2);
    }