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 your CView
-derived class declaration: #include "FilteringListCtrl.h"
class CApplicationView : public CView
{
...
protected:
...
CFilteringListCtrl listCtrl;
};
- Create a list control:
void CApplicationView::OnInitialUpdate()
{
...
CRect rc;
GetClientRect(rc);
listCtrl.Create(
WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL,
CRect(0, 0, rc.right - rc.left, rc.bottom - rc.top), this, id);
listCtrl.SetExtendedStyle(...);
listCtrl.CreateEx(this,
111);
...
}
- Create columns for the list control:
int ndx_dummy = listCtrl.InsertColumn(0, "Dummy", LVCFMT_LEFT, 200);
listCtrl.InsertColumn(1, "Column #1", LVCFMT_CENTER, 200);
listCtrl.InsertColumn(2, "Column #2", LVCFMT_CENTER, 200);
listCtrl.InsertColumn(3, "Column #3", LVCFMT_CENTER, 200);
listCtrl.DeleteColumn(ndx_dummy);
- Setup filters:
listCtrl.SetFilter(
0,
"Item 5",
"Item 20",
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,
UINT id)
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(
int index,
int subindex,
CString str)
SetFilter
: Installs a new filter. Returns true
if filter was successfully installed, false
on failure. bool CFilteringListCtrl::SetFilter(
int nColumn,
CString upper,
CString lower,
bool direction)
GetFilter
: Retrieves information about the specific filter. Returns true
if filter was found, false
if it wasn't. bool CFilteringListCtrl::GetFilter(
int nColumn,
CString& upper,
CString& lower,
bool* direction)
RemoveFilter
: Removes the specified filter. Returns true
on success, false
if filter was not found. bool CFilteringListCtrl::RemoveFilter(int nColumn)
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)
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;
this->EnableToolTips(true);
return 0;
}
- Filter core.
Three arrays:
class CFilteringListCtrl : public CListCtrl
{
...
protected:
CStringArray Filters_From;
CStringArray Filters_To;
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);
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
{
...
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:
int CFilteringListCtrl::OnToolHitTest(CPoint point,
TOOLINFO * pTI) const
{
LVHITTESTINFO lvhitTestInfo;
lvhitTestInfo.pt = point;
int nItem = ListView_SubItemHitTest(this->m_hWnd, &lvhitTestInfo);
if(nItem < 0)
return -1;
int nSubItem = lvhitTestInfo.iSubItem;
LVITEM item;
TCHAR buffer[128];
item.iItem = nItem;
item.iSubItem = nSubItem;
item.pszText = buffer;
item.cchTextMax = 128;
item.mask = LVIF_TEXT;
GetItem(&item);
if(CheckStringAgainstFilter(item.pszText, nSubItem))
return -1;
if(this->Filters_From.GetSize() <= nSubItem)
return -1;
if(lvhitTestInfo.flags)
{
RECT rcClient;
GetClientRect(&rcClient);
pTI->hwnd = m_hWnd;
pTI->uId = (UINT)(nItem * 1000 + nSubItem + 1);
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 \"");
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:
void CFilteringListCtrl::OnClick(NMHDR* pNMHDR, LRESULT* pResult)
{
NMLISTVIEW* nmhdr = (NMLISTVIEW*)pNMHDR;
if(nmhdr->iItem < 0)
{
*pResult = 0;
return;
}
last_selected_item = nmhdr->iItem;
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);
}
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);
}
}
*pResult = 1;
}
void CFilteringListCtrl::OnKeydown(NMHDR* pNMHDR, LRESULT* pResult)
{
LV_KEYDOWN* pLVKeyDow = (LV_KEYDOWN*)pNMHDR;
int nItem = last_selected_item, nNextItem =
last_selected_item, i = 1;
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:
if(nItem > 0)
{
for(; i <= nItem; i++)
{
if(CheckItemAgainstAllFilters(nItem - i))
{
nNextItem = nItem - i;
break;
}
}
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:
}
last_selected_item = nNextItem;
SetItemState(last_selected_item, LVIS_FOCUSED, LVIS_FOCUSED);
for(i = 0; i < GetItemCount(); i++)
{
if(!CheckItemAgainstAllFilters(i))
{
SetItemState(i, 0, LVIS_SELECTED);
SetItemState(i, 0, LVIS_FOCUSED);
}
}
*pResult = 1;
}
History
- June 15th - initial release.