Advanced item filtering within CListCtrl






3.72/5 (9 votes)
Jun 21, 2006
3 min read

59222

1224
This article presents a "range filtering"-capable modification of the MFC list view control, along with some other minor techniques.
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:
- Include the
CFilteringListCtrl
object in yourCView
-derived class declaration:#include "FilteringListCtrl.h" class CApplicationView : public CView { ... protected: ... CFilteringListCtrl listCtrl; };
- 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. ... }
- 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);
- 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 returnfalse
.
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 byCListCtrl
, 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
'sWM_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 multipleInsertItem
/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. Returnstrue
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)
GetFilter
: Retrieves information about the specific filter. Returnstrue
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. Returnstrue
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. Returnstrue
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.