CListCtrl 中的高级项过滤






3.72/5 (9投票s)
2006 年 6 月 21 日
3分钟阅读

59767

1224
本文介绍了一种 MFC 列表视图控件的“范围过滤”功能修改,以及其他一些小的技巧。
引言
有时,您可能需要过滤列表控件中的信息,即删除不符合某些标准的项。其他时候,不删除项而查看哪些项通过(或未通过)了过滤器以及为什么会通过(或未通过)会更有用。本文介绍的 CListCtrl
修改允许您轻松地为报表式列表视图应用各种过滤器。当前代码仅限于范围过滤器(即,由上限和下限指定的过滤器),但可以轻松地改编以支持其他类型的过滤器,例如模式过滤器。
安装
安装很简单
- 在您的
CView
派生类声明中包含CFilteringListCtrl
对象#include "FilteringListCtrl.h" class CApplicationView : public CView { ... protected: ... CFilteringListCtrl listCtrl; };
- 创建列表控件
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. ... }
- 为列表控件创建列
// 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);
- 设置过滤器
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:如果您希望列表控件填满父窗口的整个客户区,强烈建议您覆盖
CView
的WM_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)
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 日 - 初始发布。