65.9K
CodeProject 正在变化。 阅读更多。
Home

CListCtrl 和排序行

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.42/5 (25投票s)

2008年7月14日

CPOL

6分钟阅读

viewsIcon

137159

downloadIcon

6453

MFC 列表控件中排序行的示例。

引言

Microsoft 的 CListCtrl 支持使用报告样式在网格中显示数据,但需要进行一些更改才能启用行排序。

本文将演示以下内容

  • 如何在 CListCtrl 中排序项目。
  • 如何在保持 Windows XP/Vista 外观的同时,在列标题中显示排序顺序。

Picture of the demo application

背景

有许多高级网格控件可以扩展 CListCtrl 以实现列排序。但是,由于这些网格控件可能非常复杂,因此很难看清它们的工作原理。

文章 Sort List Control 仅演示了一种执行排序的方法(使用 CListCtrl::SortItems)。

如何在 CListCtrl 中排序项目

通常有三种向 CListCtrl 插入数据的方法,这也会影响我们选择的排序方法。

  • 每个单元格的文本通过 CListCtrl::SetItemText() 提供。这将把值存储在 CListCtrl 内部,并且只能手动更新这些值。当然,这会占用额外的内存,因为整个数据模型现在存储了两次。
  • 而不是将整个数据模型的副本存储在 CListCtrl 中,我们可以告诉 CListCtrl 使用回调来检索单元格文本。为了实现这一点,我们可以使用 LPSTR_TEXTCALLBACK 调用 CListCtrl::SetItemText(),它会在每次需要知道某个单元格的值时发送一个 LVN_GETDISPINFO 消息。
  • 虚拟列表是回调方法的扩展,其中 CListCtrl 只被告知整个数据模型中有多少项。CListCtrl 成为数据模型的镜像,但通常它只镜像整个数据模型的缓存。

当使用从 CListCtrl 到数据模型的​​回调时,需要考虑一些额外的因素,因为 LVN_GETDISPINFO 消息使用 CListCtrl 的行和列索引。

  • 每次执行行排序时,行索引都会发生变化,因此我们必须使用 CListCtrl::SetItemData() 为每个插入的行分配一个唯一的标识符。唯一标识符应对应于数据模型中的对象。收到 LVN_GETDISPINFO 消息时,可以使用 CListCtrl::GetItemData() 将行索引转换回唯一标识符。
  • 列索引必须是连续的,因此我们应该强烈考虑使用 CListCtrl::InsertColumn() 的最后一个参数 nSubItem 为每个插入的列分配一个唯一的标识符。当我们需要对列配置进行持久化,同时又允许行被删除和添加时,这个好处就显现出来了。收到 LVN_GETDISPINFO 消息时,可以使用掩码 LVCF_SUBITEMCListCtrl::GetColumn() 将列索引转换回唯一标识符。
使用 CListCtrl::SortItemsEx

如果将每个单元格的文本存储在 CListCtrl 中,最好的选择是使用 SortItemsEx() 并将一个排序函数作为参数。排序函数将获取要比较的两个单元格的行索引。

namespace {
    struct PARAMSORT
    {
        PARAMSORT(HWND hWnd, int columnIndex, bool ascending)
            :m_hWnd(hWnd)
            ,m_ColumnIndex(columnIndex)
            ,m_Ascending(ascending)
        {}

        HWND m_hWnd;
        int  m_ColumnIndex;
        bool m_Ascending;
    };

    // Comparison extracts values from the List-Control
    int CALLBACK SortFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
    {
        PARAMSORT& ps = *(PARAMSORT*)lParamSort;

        TCHAR left[256] = _T(""), right[256] = _T("");
        ListView_GetItemText(ps.m_hWnd, lParam1, 
          ps.m_ColumnIndex, left, sizeof(left));
        ListView_GetItemText(ps.m_hWnd, lParam2, 
          ps.m_ColumnIndex, right, sizeof(right));    

        if (ps.m_Ascending)
            return _tcscmp( left, right );
        else
            return _tcscmp( right, left );            
    }
}

bool CListCtrl_SortItemsEx::SortColumn(int columnIndex, bool ascending)
{
    PARAMSORT paramsort(m_hWnd, columnIndex, ascending);
    ListView_SortItemsEx(m_hWnd, SortFunc, ¶msort);
    return true;
}
使用 CListCtrl::SortItems

如果使用回调在 CListCtrl 中显示数据,我们可以使用 SortItems() 并将一个排序函数作为参数。排序函数将获取要比较的两个单元格的行项目数据。

当数据包含在 CListCtrl 中时,有时也使用此方法,但不推荐这样做,因为将项目数据转换为正确的行索引速度很慢。

namespace {
    struct PARAMSORT
    {
        PARAMSORT(const CListCtrl_DataModel& datamodel, 
                  int columnData, bool ascending)
            :m_DataModel(datamodel)
            ,m_ColumnData(columnData)
            ,m_Ascending(ascending)
        {}

        const CListCtrl_DataModel& m_DataModel;
        int  m_ColumnData;
        bool m_Ascending;
    };

    // Comparison extracts values from the DataModel
    int CALLBACK SortFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
    {
        PARAMSORT& ps = *(PARAMSORT*)lParamSort;

        const string& left = 
          ps.m_DataModel.GetCellText((size_t)lParam1, ps.m_ColumnData);
        const string& right = 
          ps.m_DataModel.GetCellText((size_t)lParam2, ps.m_ColumnData);

        if (ps.m_Ascending)
            return _tcscmp( left.c_str(), right.c_str() );    
        else
            return _tcscmp( right.c_str(), left.c_str() );                    
    }
}

bool CListCtrl_SortItems::SortColumn(int columnIndex, bool ascending)
{
    if (GetItemCount()!=m_DataModel.GetRowIds())
        return false;

    int columnData = GetColumnData(columnIndex);
    PARAMSORT paramsort(m_DataModel, columnData, ascending);
    SortItems(SortFunc, (DWORD_PTR)¶msort);
    return true;
}
使用稳定排序

如果使用回调在 CListCtrl 中显示数据,并且数据模型容器查找单个项目“很慢”,那么我们可以考虑缓存整个列的内容,然后对它们进行排序,而无需查找的开销。

  1. 为每一行创建一个包含项目数据和列单元格文本的临时容器。
  2. 根据列单元格文本对容器进行排序。
  3. 遍历所有行,并按照容器的顺序调用 CListCtrl::SetItemData()。现在,CListCtrl 中的第一行将具有根据所选排序顺序的第一行的项目数据。

stable_sort() 需要更多的工作,因为我们应该尝试保留 CListCtrl 中的原始行顺序。演示应用程序的源代码包含了一个如何实现它的示例。

namespace {
    bool AscSortFunc(const pair<string,size_t>& left, 
                     const pair<string,size_t>& right)
    {
        return left.first < right.first;
    }
    bool DescSortFunc(const pair<string,size_t>& left, 
                      const pair<string,size_t>& right)
    {
        return right.first < left.first;
    }
}

bool CListCtrl_StableSort::SortColumn(int columnIndex, bool ascending)
{
    // Sorting optimized for a datamodel where lookup is slow
    //    - Uses more memory during sort, because it takes a copy of an entire column
    //    - Even faster if one can iterate over the datamodel without lookup
    int columnData = GetColumnData(columnIndex);
    
    // Extract entire column from datamodel
    vector< pair<string,size_t> > entireColumn;
    entireColumn.reserve( m_DataModel.GetRowIds() );
    for(size_t rowId = 0; rowId < m_DataModel.GetRowIds(); ++rowId)
    {
        entireColumn.push_back( make_pair(m_DataModel.GetCellText(rowId, 
                                          columnData),rowId) );
    }

    // Sort entire column
    if (ascending)
        sort(entireColumn.begin(), entireColumn.end(), AscSortFunc);
    else
        sort(entireColumn.begin(), entireColumn.end(), DescSortFunc);

    // Update list-control with new column-order
    for(int nItem = 0; nItem < GetItemCount(); ++nItem)
    {
        SetItemData(nItem, entireColumn[nItem].second);
    }

    return true;
}
使用排序样式

首先,Microsoft 不推荐使用 LVS_SORTASCENDINGLVS_SORTDESCENDING 样式进行排序,因为它不支持本地字符。更多信息:MS KB191295

  • 它不支持 LPSTR_TEXTCALLBACK,因此文本不能通过回调提供,而必须直接使用 CListCtrl::SetItemText() 插入。
  • 它只在标签列(第一个插入的列)上工作。
  • 它只在插入项目时进行排序,因此在项目插入后无法更改排序顺序。

如果有一个静态列表,并且它只需要根据单列进行排序,那么这个选项非常容易实现。

void CListCtrl_SortStyle::LoadData(const CListCtrl_DataModel& dataModel)
{
    // Must decide sort-order before inserting !
    if (IsAscending())
        ModifyStyle(NULL, LVS_SORTASCENDING);
    else
        ModifyStyle(NULL, LVS_SORTDESCENDING);

    for(size_t rowId = 0; rowId < dataModel.GetRowIds() ; ++rowId)
    {
        // When inserting item, then one must provide cell-text
        InsertItem(++nItem, dataModel.GetCellText(rowId, 0).c_str());
    }
}
使用虚拟列表

通过应用所有者数据样式 LVS_OWNERDATA,可以将 CListCtrl 转换为虚拟列表。由于虚拟列表与数据模型完全同步,因此我们必须根据所需的列排序顺序对数据模型中的记录进行排序。

通常,CListCtrl 只会镜像整个数据模型的一小部分缓存,因此排序操作变成了对整个数据模型中与排序条件匹配的可见行的查询。示例代码不包含如何做到这一点,因为实现数据模型缓存系统超出了本文的范围。

如何显示列标题中的排序顺序箭头

当用户单击列标题以根据所选列对所有行进行排序时,列标题应更改以反映实际的排序顺序。

Windows XP 引入了列标题项样式 HDF_SORTDOWNHDF_SORTUP,这使得显示排序顺序变得相对容易。在此之前,我们可以选择将位图图像加载到列标题中,或者执行列标题的所有者绘图。挑战在于检测 Windows XP 是否可以帮助显示排序顺序箭头,或者我们是否必须使用位图图像(HDF_BITMAP)。

有关如何更新列标题的详细信息已由 Massimo Galbusera 回答,并且许多更高级的从 CListCtrl 扩展的网格控件都使用了这种技术。

使用代码

提供的源代码包括了五种不同排序方法的实现,因此我们可以看到不同的方式在实际应用中的效果。

关注点

在开发演示应用程序时,非虚拟列表实现的项目插入存在一些性能问题。通过使用 SetRedraw() 函数解决了这个问题,并且通过确保在新行插入到列表底部,还提高了速度。

void CListCtrl_SortItemsEx::LoadData(const CListCtrl_DataModel& dataModel)
{
    // Insert data into list-control by copying from datamodel
    SetRedraw(FALSE);    // Disable redraw as InsertItem becomes so much faster
    int nItem = 0;
    for(size_t rowId = 0; rowId < dataModel.GetRowIds() ; ++rowId)
    {
        nItem = InsertItem(++nItem, "");    // Faster to insert at the end
        SetItemData(nItem, rowId);
        for(int col = 0; col < dataModel.GetColCount() ; ++col)
        {
            SetItemText(nItem, col, dataModel.GetCellText(rowId, col).c_str());
        }
    }
    SetRedraw(TRUE);
    Invalidate();
    UpdateWindow();
}

历史

  • 2008-07-14 - 文章首次发布。
© . All rights reserved.