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

CListCtrl 中的高级项过滤

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.72/5 (9投票s)

2006 年 6 月 21 日

3分钟阅读

viewsIcon

59767

downloadIcon

1224

本文介绍了一种 MFC 列表视图控件的“范围过滤”功能修改,以及其他一些小的技巧。

Three columns - three filters

引言

有时,您可能需要过滤列表控件中的信息,即删除不符合某些标准的项。其他时候,不删除项而查看哪些项通过(或未通过)了过滤器以及为什么会通过(或未通过)会更有用。本文介绍的 CListCtrl 修改允许您轻松地为报表式列表视图应用各种过滤器。当前代码仅限于范围过滤器(即,由上限和下限指定的过滤器),但可以轻松地改编以支持其他类型的过滤器,例如模式过滤器

安装

安装很简单

  1. 在您的 CView 派生类声明中包含 CFilteringListCtrl 对象
    #include "FilteringListCtrl.h"
    
    class CApplicationView : public CView
    {
       ...
    
    protected:
    
       ...
    
       CFilteringListCtrl listCtrl;
    
    };
  2. 创建列表控件
    void CApplicationView::OnInitialUpdate()
    {
       ...
    
       // You can either:
       CRect rc;
       GetClientRect(rc);
    
       listCtrl.Create(
         WS_CHILD | /*WS_BORDER |*/ WS_VISIBLE | WS_VSCROLL | WS_HSCROLL,
         CRect(0, 0, rc.right - rc.left, rc.bottom - rc.top), this, id);
       listCtrl.SetExtendedStyle(...);
    
       // or (which is easier, sets up the most common options):
       listCtrl.CreateEx(this,      // Parent CWnd*.
                         111);      // ID for this list control.
    
       ...
    }
  3. 为列表控件创建列
    // Here is a little trick. To make 1st column center-justified:
    // 1. create a dummy column:
    int ndx_dummy = listCtrl.InsertColumn(0, "Dummy", LVCFMT_LEFT, 200);
    
    // 2. create all other columns:
    listCtrl.InsertColumn(1, "Column #1", LVCFMT_CENTER, 200);
    listCtrl.InsertColumn(2, "Column #2", LVCFMT_CENTER, 200);
    listCtrl.InsertColumn(3, "Column #3", LVCFMT_CENTER, 200);
    
    // 3. remove dummy column:
    listCtrl.DeleteColumn(ndx_dummy);
  4. 设置过滤器
    listCtrl.SetFilter(
      // Column index (zero-based), which filter will be bound to.
      0,
      // Upper bound for a filter.
      "Item 5",
      // Lower bound for a filter.
      "Item 20",
      // Filter direction: internal (true) / external (false).
      true);
    
    listCtrl.SetFilter(1, "7",  "10", false);
    listCtrl.SetFilter(2, "14", "18", false);

    您必须在插入列之后设置过滤器;否则,SetFilter 方法将不起作用,并返回 false

新 API

  • CreateEx:创建一个支持过滤的列表视图控件,将其大小调整为适合父窗口,并设置最常用的扩展样式。成功时返回非零值;否则返回 0。
    int CFilteringListCtrl::CreateEx(CWnd* parent, // Parent window.
                                     UINT id) // ID of the list control.

    注意 1:这不是 CListCtrl 提供的 CreateEx 方法,请小心。

    注意 2:如果您希望列表控件填满父窗口的整个客户区,强烈建议您覆盖 CViewWM_SIZE 消息的默认行为。

    void CApplicationView::OnSize(UINT nType, int cx, int cy) 
    {
       CView::OnSize(nType, cx, cy);
    
       CRect rc;
       this->GetClientRect(rc);
    
       if(listCtrl) listCtrl.MoveWindow(rc);
    }
  • InsertItemEx:将字符串值插入指定的项/列。这是一个方便的方法,可避免处理多个 InsertItem/SetItem 调用。返回已插入/更改的项的零基索引,失败时返回 -1
    int CFilteringListCtrl::InsertItem(
      // Item index (zero-based).
      int index,
      // Column index.
      int subindex,
      // String to insert.
      CString str)
  • SetFilter:安装新过滤器。成功安装过滤器时返回 true,失败时返回 false
    bool CFilteringListCtrl::SetFilter(
      // Column index (zero-based), which filter will be bound to.
      int nColumn,
      // Upper bound for a filter.
      CString upper,
      // Lower bound for a filter.
      CString lower,
      // Filter direction: internal (true) / external (false).
      bool direction)

    Interior and exterior filters

  • GetFilter:检索有关特定过滤器信息。找到过滤器时返回 true,未找到过滤器时返回 false
    bool CFilteringListCtrl::GetFilter(
       // Index (zero-based) of the filter to retrieve information of.
       int nColumn,
       // Upper bound for a filter.
       CString& upper,
       // Lower bound for a filter.
       CString& lower,
       // Filter direction.
       bool* direction)
  • RemoveFilter:删除指定的过滤器。成功时返回 true,过滤器未找到时返回 false
    bool CFilteringListCtrl::RemoveFilter(int nColumn)
    // Index (zero-based) of the filter to remove.
  • CheckItemAgainstAllFilters:用于手动将特定项与已安装的过滤器进行比较。如果项通过所有过滤器,则返回 true,否则返回 false
    bool CFilteringListCtrl::CheckItemAgainstAllFilters(int iItem) 
    // Index (zero-based) of the item to check.

内部机制

尽管代码流程相当直接,但有些地方值得一提。

  • 启动。

    没什么神奇的。

    BOOL CFilteringListCtrl::PreCreateWindow(CREATESTRUCT& cs) 
    {
        cs.style |= LVS_REPORT | LVS_OWNERDRAWFIXED;
    
        return CListCtrl::PreCreateWindow(cs);
    }
    
    int CFilteringListCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct) 
    {
        if (CListCtrl::OnCreate(lpCreateStruct) == -1)
            return -1;
    
            // REQUIRED if you wish custom tooltips to work:
        this->EnableToolTips(true);
    
        return 0;
    }
  • 过滤器核心。

    三个数组

    class CFilteringListCtrl : public CListCtrl
    {
       ...
    
    protected:
    
       // Filter core:
       // Upper bounds
       CStringArray Filters_From;
       // Lower bounds
       CStringArray Filters_To;
       // Filters' directions: internal
       //    (true) or external (false)
       CByteArray   Filters_Direction;
    
       ...
    };

    和两个函数

    class CFilteringListCtrl : public CListCtrl
    {
    public:
    
       ...
    
       bool CheckItemAgainstAllFilters(int iItem);
    
    protected:
    
       ...
    
       bool CheckStringAgainstFilter(CString str, int nFilter) const;
    };

    是过滤器子系统的核心。因此,要实现新的过滤算法,您应该

    • 定义一个新的过滤器核心 - 存储过滤器状态的成员变量;
    • 重写 GetFilter/SetFilter/RemoveFilter 成员函数;
    • 重新实现 CheckStringAgainstFilter

    这些是您的主要目标;还有一些次要任务,例如修正 OnToolHitTest 等。

  • 比较项。

    CheckStringAgainstFilter 方法围绕一个简单的比较构建。

    bool CFilteringListCtrl::CheckStringAgainstFilter(CString str, 
                                                      int nFilter) const
    {
       CString from = this->Filters_From.GetAt(nFilter);
       CString to = this->Filters_To.GetAt(nFilter);
    
       ...
    
       if((str.Compare(from) >= 0) && (str.Compare(to) <= 0))
         return (this->Filters_Direction.GetAt(nFilter) != 0);
    }

    但更大的麻烦是

    CString str1 = "Item 9";
    CString str2 = "Item 10";
    
    str2.Compare(str1);
    // returns -1, i.e. str2 is <U>less</U> than str1.

    问题在于 Compare 方法是以字典顺序比较字符串的,这意味着:

    • 逐个元素进行比较。
    • 直到函数找到两个不相等的对应元素,并将其比较结果作为序列间比较结果时,才会进行比较。(所以,在我们的例子中,当 Compare 函数找到“Item 9”中的“9”和“Item 10”中的“1”这一对时,它会停止执行,并将“Item 9”视为大于“Item 10”。)
    • 如果没有找到不相等项,但一个序列比另一个序列长,则较短的序列被视为小于较长的序列。
    • 如果没有找到不相等项且序列具有相同的元素数量,则认为序列相等,比较结果为零。

    通常,您(以及您的常识)可能希望 Compare 给出相反的结果,即“Item 10”必须大于“Item 9”。解决方案很简单。

    bool CFilteringListCtrl::CheckStringAgainstFilter(CString str, int nFilter) const
    {
    
    // Uncomment if you wish to return to a "classic" string comparison:
    // return CheckStringAgainstFilterLexicographical(str, nFilter);
    
       ...
    
       int compare_from = 0, compare_to = 0;
    
       if(str.GetLength() > from.GetLength())
         compare_from = 1;
       else if(str.GetLength() < from.GetLength())
         compare_from = -1;
       else
         compare_from = str.Compare(from);
    
       if(str.GetLength() > to.GetLength())
         compare_to = 1;
       else if(str.GetLength() < to.GetLength())
         compare_to = -1;
       else
         compare_to = str.Compare(to);
    
       if((compare_from >= 0) && (compare_to <= 0))
         return (this->Filters_Direction.GetAt(nFilter) != 0);
  • 自定义工具提示。

    令人惊讶的是,许多人不知道有一个可重写的 OnToolHitTest 方法——这是显示每个项工具提示的最简单方法。

    // Before using this, be sure to EnableToolTips(true) in OnCreate!
    int CFilteringListCtrl::OnToolHitTest(CPoint point, 
                                          TOOLINFO * pTI) const
    {
       // Retrieve the item index under the mouse pointer:
       LVHITTESTINFO lvhitTestInfo;
       lvhitTestInfo.pt = point;
    
       int nItem = ListView_SubItemHitTest(this->m_hWnd, &lvhitTestInfo);
    
       // If no item, ignore this:
       if(nItem < 0)
         return -1;
    
       // Index of the column we're pointing:
       int nSubItem = lvhitTestInfo.iSubItem;
    
       // Retrieve item text:
       LVITEM item;
       TCHAR buffer[128];
    
       item.iItem = nItem;
       item.iSubItem = nSubItem;
       item.pszText = buffer;
       item.cchTextMax = 128;
       item.mask = LVIF_TEXT;
    
       GetItem(&item);
    
       // We're only interested in "invalid" items:
       if(CheckStringAgainstFilter(item.pszText, nSubItem))
         return -1;
    
       if(this->Filters_From.GetSize() <= nSubItem)
         return -1;
    
       if(lvhitTestInfo.flags)
       {
         RECT rcClient;
         GetClientRect(&rcClient);
    
         // Fill in the TOOLINFO structure
         pTI->hwnd = m_hWnd;
         pTI->uId = (UINT)(nItem * 1000 + nSubItem + 1);
    
         // Construct a tooltip string:
         CString filter = this->Filters_From.GetAt(nSubItem);
         if(filter.IsEmpty())
           return -1;
    
         filter += "\" to \"" +  this->Filters_To.GetAt(nSubItem)
                              + (this->Filters_Direction.GetAt(nSubItem) ? 
                              "\" are valid." : "\" are invalid.");
         filter.Insert(0, " filtered by condition: values from \"");
         filter.Insert(0, CString(buffer) + "\"");
         filter.Insert(0, "Value \"");
    
         // There is no memory leak here - MFC frees
         // the memory after it has added the tool.
         // See MSDN KB 156067 for more info.
         pTI->lpszText = (char*)malloc(filter.GetLength() + 1);
         strcpy(pTI->lpszText, filter.GetBuffer(filter.GetLength()));
    
         pTI->rect = rcClient;
    
         return pTI->uId;
       }
       else
         return -1;
    }
  • 导航。

    在找到“无效”项(未通过过滤器的项)后,我们必须确保用户无法选择或聚焦它们。因此,我们必须彻底完善列表控件的鼠标/键盘导航和选择算法。

    // Main message from the mouse:
    void CFilteringListCtrl::OnClick(NMHDR* pNMHDR, LRESULT* pResult)
    {
       NMLISTVIEW* nmhdr = (NMLISTVIEW*)pNMHDR;
    
       if(nmhdr->iItem < 0)
       {
         *pResult = 0;
         return;
       }
    
       last_selected_item = nmhdr->iItem;
    
       // Is this the beginning of the selection?
       if(HIBYTE(GetKeyState(VK_SHIFT)))
       {
         first_in_a_row = first_in_a_row == -1 ? 
                          last_selected_item : first_in_a_row;
         SetSelectionMark(first_in_a_row);
       }
       else
         first_in_a_row = -1;
    
       if(CheckItemAgainstAllFilters(nmhdr->iItem))
       {
         SetItemState(nmhdr->iItem, LVIS_SELECTED, LVIS_SELECTED);
         SetItemState(nmhdr->iItem, LVIS_FOCUSED, LVIS_FOCUSED);
       }
    
       // Check if current selection is valid:
       if(first_in_a_row != -1)
       {
         for(int i = 0; i < GetItemCount(); i++)
         {
           if(first_in_a_row < last_selected_item)
             SetItemState(i, (CheckItemAgainstAllFilters(i) &&
                             (i <= last_selected_item && i >= 
                             first_in_a_row) ? LVIS_SELECTED : 0), 
                             LVIS_SELECTED);
           else
             SetItemState(i, (CheckItemAgainstAllFilters(i) &&
                             (i >= last_selected_item && 
                              i <= first_in_a_row) ? 
                              LVIS_SELECTED : 0), LVIS_SELECTED);
         }
       }
    
       // Cancel default OnClick handling:
       *pResult = 1;
    }
    
    // Main message from the keyboard:
    void CFilteringListCtrl::OnKeydown(NMHDR* pNMHDR, LRESULT* pResult)
    {
       LV_KEYDOWN* pLVKeyDow = (LV_KEYDOWN*)pNMHDR;
    
       int nItem = last_selected_item, nNextItem = 
                   last_selected_item, i = 1;
    
       // Is this the beginning of the selection?
       if(HIBYTE(GetKeyState(VK_SHIFT)))
       {
         if(first_in_a_row == -1)
         {
           first_in_a_row = last_selected_item;
           SetSelectionMark(first_in_a_row);
         }
       }
       else
         first_in_a_row = -1;
    
       switch(pLVKeyDow->wVKey)
       {
         case VK_UP:
    
           // Search for a "valid" item that
           // is higher that current item:
           if(nItem > 0)
           {
             for(; i <= nItem; i++)
             {
               if(CheckItemAgainstAllFilters(nItem - i))
               {
                 nNextItem = nItem - i;
                 break;
               }
             }
    
             // Setting up the selection:
             for(i = 0; i < GetItemCount(); i++)
             {
               if(first_in_a_row == -1)
                 SetItemState(i, (i == nNextItem ? 
                                  LVIS_SELECTED : 0), LVIS_SELECTED);
               else
               {
                 if(first_in_a_row < nNextItem)
                   SetItemState(i, (i <= nNextItem && 
                                i >= first_in_a_row ? LVIS_SELECTED : 0), 
                                LVIS_SELECTED);
                 else
                   SetItemState(i, (i >= nNextItem && i <= 
                                first_in_a_row ? LVIS_SELECTED : 0), 
                                LVIS_SELECTED);
               }
             }
           }
           break;
    
         case VK_DOWN:
    
             // ... nearly the same as before ...
       }
    
       // Setting focus to the found/same item:
       last_selected_item = nNextItem;
       SetItemState(last_selected_item, LVIS_FOCUSED, LVIS_FOCUSED);
    
       // Check if current selection is valid:
       for(i = 0; i < GetItemCount(); i++)
       {
         if(!CheckItemAgainstAllFilters(i))
         {
           SetItemState(i, 0, LVIS_SELECTED);
           SetItemState(i, 0, LVIS_FOCUSED);
         }
       }
    
       // Cancel default OnKeyDown handling:
       *pResult = 1;
    }

历史

  • 6 月 15 日 - 初始发布。
© . All rights reserved.