尝试在 C 语言的 WinAPI ListView 中对值进行排序,代码基于示例、MSDN 和包括 SO 在内的多个问答论坛。创建列和插入项的代码如下所示:
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");
然后,比较器函数和事件处理程序如下:
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);
}
我注意到,如果我将指针传递给 InsertItem 函数而不是硬编码文本,那么单击列会产生效果(它"有点"排序,不完全符合预期,但项目的顺序会发生变化(。如果只有硬编码文本,则单击标题不起作用。
问题 1:为什么这不适用于硬编码值?
问题 2:在 C 语言中比较字符串比 strcmp 有没有"更好"的函数?这是排序没有给出预期结果的原因吗(排序不正确,例如 10 -> 30 -> 20 永远不会排序(?
编辑在调试器(Win10 上的 VS2019(中,比较器函数中的值似乎编码错误。不知何故,看起来参数类型在某处丢失了。
lvi.lParam
可以是整数值或指针。如果它是一个指针,那么它通常指向堆中分配的数据。
此外,列表视图中的每一行只能与一个lvi.lParam
相关联。您似乎正在尝试为每行中的每一列关联单独的lvi.lParam
数据。但这无法完成,因此对ListView_SetItem
的第二次调用将失败,因为设置了LVIF_PARAM
,并且lvi.iSubItem
大于零。此代码将不起作用:
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
解决方案 1:使用ListView_SortItemsEx
:
我们忽略lParam
,我们使用ListView_GetItemText
来获取比较函数中的文本:
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);
}
解决方案2:使用
ListView_SortItems
:
我们可以为字符串分配单独的内存,然后将lParam
值关联到该字符串分配。
此方法更复杂,但它可以更快,因为我们对ListView_GetItemText
进行N
调用(其中N
是 ListView 中的总数(。在以前的解决方案中,我们ListView_GetItemText
进行N * log(N)
调用,如果列表太大,可能会有明显的差异(尽管我还没有测试过(
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;
}
编辑,我们可以更改InsertItem
函数,以便ListView_InsertItem
返回一个索引,然后在ListView_SetItemText
中使用该索引。
一般来说,这会更安全。例如,如果设置了LVS_SORTASCENDING/LVS_SORTDESCENDING
,则ListView_InsertItem
返回的索引与请求的行不同。
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);
}