Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Advanced item filtering within CListCtrl

0.00/5 (No votes)
21 Jun 2006 1  
This article presents a "range filtering"-capable modification of the MFC list view control, along with some other minor techniques.

Three columns - three filters

Introduction

Sometimes, you may have a task to filter information in your list control, i.e., to remove items that do not meet some criteria. Other times, it is useful not to remove items, but to see which items passed (and/or did not pass) the filter and why. The presented modification of CListCtrl allows you to easily apply various filters to report-style list views. The current code is limited to range filters (that is, filters specified by upper and lower bounds), but can be easily adopted to work with other types of filters, like pattern filters.

Setup

Installation is simple:

  1. Include the CFilteringListCtrl object in your CView-derived class declaration:
    #include "FilteringListCtrl.h"
    
    
    class CApplicationView : public CView
    {
       ...
    
    protected:
    
       ...
    
       CFilteringListCtrl listCtrl;
    
    };
  2. Create a list control:
    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. Create columns for the list control:
    // 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. Setup filters:
    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);

    You must setup filters after you've inserted columns; otherwise, the SetFilter method will do nothing, and return false.

New API

  • CreateEx: Creates a filter-aware list view control, sizes it to fit the parent window, and sets the most commonly used extended styles. Returns a nonzero value on success; otherwise 0.
    int CFilteringListCtrl::CreateEx(CWnd* parent, // Parent window.
    
                                     UINT id) // ID of the list control.

    Note 1: this is not the CreateEx method provided by CListCtrl, so be careful.

    Note 2: if you wish the list control to fill the entire client area of a parent window, it is strongly recommended that you override the default behavior of CView's WM_SIZE message:

    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: Inserts a string value into the specified item/column. This is a convenience method to avoid handling multiple InsertItem/SetItem calls. Returns a zero-based index of the item that was inserted/changed, or -1 on failure.
    int CFilteringListCtrl::InsertItem(
      // Item index (zero-based).
    
      int index,
      // Column index.
    
      int subindex,
      // String to insert.
    
      CString str)
  • SetFilter: Installs a new filter. Returns true if filter was successfully installed, false on failure.
    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: Retrieves information about the specific filter. Returns true if filter was found, false if it wasn't.
    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: Removes the specified filter. Returns true on success, false if filter was not found.
    bool CFilteringListCtrl::RemoveFilter(int nColumn)
    // Index (zero-based) of the filter to remove.
  • CheckItemAgainstAllFilters: Use to manually check the specific item against installed filters. Returns true if item passes through all filters, false if it doesn't.
    bool CFilteringListCtrl::CheckItemAgainstAllFilters(int iItem) 
    // Index (zero-based) of the item to check.

Inner workings

Although the code flow is quite straightforward, there are some moments worth mentioning.

  • Startup.

    Nothing magic here:

    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;
    }
  • Filter core.

    Three arrays:

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

    and two functions:

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

    are the heart of the filter subsystem. So, in order to implement a new filtering algorithm, you should:

    • define a new filter core - member variables which store the state of the filter;
    • rewrite the GetFilter/SetFilter/RemoveFilter member functions;
    • re-implement CheckStringAgainstFilter.

    These are your primary targets; there are some minor tasks, like correcting the OnToolHitTest and so on.

  • Comparing items.

    The CheckStringAgainstFilter method is built around a simple comparison:

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

    But the big trouble is:

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

    The problem is that the Compare method compares strings lexicographically, which means that:

    • Comparison is performed element by element.
    • Comparison is performed until the function finds two corresponding elements unequal, and the result of their comparison is taken as the result of the comparison between the sequences. (So when, in our example, the Compare function finds a pair of "9" (in "Item 9") and "1" (in "Item 10"), it stops execution, and considers "Item 9" to be greater than "Item 10".)
    • If no inequalities are found, but one sequence has more elements than the other, then the shorter sequence is considered less than the longer sequence.
    • If no inequalities are found and the sequences have the same number of elements, the sequences are considered equal and the result of the comparison is zero.

    Normally, you (and your common sense) may wish Compare to give you the opposite result, i.e., "Item 10" must be greater than "Item 9". The solution is simple:

    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);
  • Custom tooltips.

    A surprising number of people are unaware of an overridable OnToolHitTest method - the most simple way of displaying per-item tooltips:

    // 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;
    }
  • Navigation.

    Having found the "invalid" items (ones that didn't pass the filter), we must be sure that users will not be able to select or focus on them. Therefore, we must completely refine the mouse/keyboard navigation and selection algorithms for our list control:

    // 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;
    }

History

  • June 15th - initial release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here