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.
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
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);
}
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);
}