CListCtrlExt 和 CListViewExt 控件
一个基于标准 CListCtrl MFC 类的增强型列表控件。
引言
MFC 中最重要和最有用的控件之一(而且不是唯一的一个)是列表控件。很长一段时间以来,我一直在寻找一个好的、完全兼容 MFC 的控件,但没有找到,我从几篇文章(和网站)收集了信息,并自己起草了一个,它
- 不具备所有标准功能(
GetItemData
、SetItemData
等) - 完全兼容 MFC(继承自
CListCtrl
) - 可用于任何样式(
LVS_ICON
、LVS_SMALLICON
、LVS_LIST
、LVS_REPORT
) - 可以对数据进行排序(并在不使用任何外部资源的情况下显示排序方向)
- 可以为单元格、行、列着色文本和/或背景
- 拥有完整的标题控件
- 持久化列宽度、顺序、外观和排序
- 可以具有网格行为
- 可以插入到各种静态控件的单元格中(例如,
CEdit
、CComboBox
、COleDateTime
等) - 可以像 MFC 标准控件
CListView
一样使用(而不是GetListCtrl()
)
所有这些只需要三个类(在 CListView
的情况下是四个)。
背景
如我上面所说,CListCtrlExt
继承自 CListCtrl
MFC 类。这意味着你可以在使用标准 CListCtrl
的项目中无修改地使用它。对于新功能,你需要调用自定义方法(选择排序的列、网格行为等)。当然,上述许多功能仅在 LVS_REPORT
样式下可用。最重要的是,这个 CListCtrlExt
类可以像 CListView
控件一样使用。你需要在项目中添加一个额外的类:CChildCListCtrlExt
类。还有一点,作为一个标准控件,外观样式(控件的主题)可以像其他任何标准控件一样处理,没有任何复杂性(额外的样式类)。
使用代码
首先,你需要将六个文件(三个类)包含到你的项目中:ListCtrlExt.h、ListCtrlExt.cpp、HeaderCtrlExt.h、HeaderCtrlExt.cpp、MsgHook.h 和 MsgHook.cpp。假设你有一个基于 CView
的 SDI 应用程序,其视图类为 CMyView
(在示例项目中为 CTestList6View
)。我们可以在动态模式下创建我们的列表(在资源头文件 Resource.h 中,定义)
#define IDC_LIST 1001
然后声明一个 CListCtrlExt
变量
// TestList6View.h : interface of the CTestList6View class
//
class CTestList6View : public CView
{
...
...
protected:
CListCtrlExt m_List;
};
// TestList6View.cpp : implementation of the CTestList6View class
//
int CTestList6View::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if(CView::OnCreate(lpCreateStruct) == -1)return -1;
// TODO: Add your specialized creation code here
DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_TABSTOP | CS_DBLCLKS | LVS_REPORT;
BOOL bResult = m_List.Create(dwStyle,CRect(0,0,0,0),this,IDC_LIST);
m_List.PreSubclassWindow();
return bResult ? 0 : -1;
// return 0;
}
void CTestList6View::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
// TODO: Add your message handler code here
if(::IsWindow(m_List.m_hWnd))m_List.MoveWindow(0,0,cx,cy,TRUE);
}
在很多情况下,我们想对列表控件中的数据进行排序。没问题,在插入列之后,我们调用 SetColumnSorting(...)
如下:
void CTestList6View::OnInitialUpdate()
{
CView::OnInitialUpdate();
// TODO: Add your specialized code here and/or call the base class
if(m_List.GetHeaderCtrl()->GetItemCount() > 0)return;
m_List.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES |
LVS_EX_HEADERDRAGDROP | LVS_EX_INFOTIP);
m_List.InsertColumn(0, "Integer", LVCFMT_LEFT, 100);
m_List.InsertColumn(1, "String", LVCFMT_LEFT, 100);
m_List.InsertColumn(2, "List", LVCFMT_LEFT, 100);
m_List.InsertColumn(3, "DateTime", LVCFMT_LEFT, 100);
m_List.InsertColumn(4, "Amount", LVCFMT_LEFT, 100);
m_List.SetColumnSorting(0, CListCtrlExt::Auto, CListCtrlExt::Int);
m_List.SetColumnSorting(1, CListCtrlExt::Auto, CListCtrlExt::String);
m_List.SetColumnSorting(2, CListCtrlExt::Auto, CListCtrlExt::StringNoCase);
m_List.SetColumnSorting(3, CListCtrlExt::Auto, CListCtrlExt::Date);
m_List.SetColumnSorting(4, CListCtrlExt::Auto, CListCtrlExt::StringNoCase);
}
要为列表着色文本或背景,你可以使用以下方法来绘制单元格、行或列:SetCellColors(...)
、SetRowColors(...)
、SetColumnColors(...)
。
如果你在 LVS_REPORT
样式中使用此列表控件,你将拥有一个完整的标题控件:右键单击标题,可以选择哪些列可见,哪些不可见,但这仅在你将至少一列设置为不可移除时可用
m_List.GetHeaderCtrl()->SetRemovable(0,FALSE);
要记住列宽度、列顺序、哪些列可见,甚至是最后一个排序的列,你需要调用两个方法:在第一次加载列表时调用 RestoreState(...)
,并在列表销毁处理程序中调用 SaveState(...)
。
如果你想要网格行为(在单个单元格中导航,在任何列中搜索),你必须做两件事:设置 LVS_EX_FULLROWSELECT
样式并调用 SetGridBehaviour()
。
此外,在动态模式下创建后,你还可以插入控件(CEdit
、CComboBox
、CDateTimeCtrl
等);在这种情况下,你需要实现两个静态方法:InitEditor(...)
和 EndEditor(...)
。
// TestList6View.h : interface of the CTestList6View class
//
protected:
CListCtrlExt m_List;
CComboBox m_Combo;
CDateTimeCtrl m_DT;
static BOOL EndEditor(CWnd** pWnd, int nRow, int nColumn, CString &strSubItemText,
DWORD_PTR dwItemData, void* pThis, BOOL bUpdate);
static BOOL InitEditor(CWnd** pWnd, int nRow, int nColumn, CString &strSubItemText,
DWORD_PTR dwItemData, void* pThis, BOOL bUpdate);
private:
CFont* m_pFont;
这是实现代码
// TestList6View.cpp : implementation of the CTestList6View class
//
void CTestList6View::OnInitialUpdate()
{
CView::OnInitialUpdate();
// TODO: Add your specialized code here and/or call the base class
m_pFont = m_List.GetFont();
CRect Rect(CPoint(0,0),CSize(100,500));
m_DT.Create(WS_CHILD | WS_TABSTOP, Rect, this, IDC_DATE);
m_Combo.Create(WS_CHILD | WS_TABSTOP | CBS_DROPDOWNLIST |
CBS_HASSTRINGS | CBS_SORT | CBS_AUTOHSCROLL,Rect,this,IDC_COMBO);
m_Combo.AddString("Test 1");
m_Combo.AddString("Test 2");
m_Combo.AddString("Test 3");
m_Combo.AddString("Test 4");
m_Combo.AddString("Test 5");
m_Combo.AddString("Test 6");
m_Combo.AddString("Test 7");
m_Combo.AddString("Test 8");
m_Combo.AddString("Test 9");
m_Combo.SetFont(m_pFont);
m_List.SetColumnEditor(2, &CTestList6View::InitEditor,
&CTestList6View::EndEditor, &m_Combo);
m_List.SetColumnEditor(3, &CTestList6View::InitEditor,
&CTestList6View::EndEditor, &m_DT);
}
BOOL CTestList6View::InitEditor(CWnd** pWnd, int nRow, int nColumn,
CString &strSubItemText, DWORD_PTR dwItemData, void* pThis, BOOL bUpdate)
{
ASSERT(*pWnd);
switch(nColumn)
{
case 2:
{
CComboBox* pCmb = reinterpret_cast<CComboBox*>(*pWnd);
pCmb->SelectString(0, strSubItemText);
}
break;
case 3:
{
CDateTimeCtrl* pDTC = reinterpret_cast<CDateTimeCtrl*>(*pWnd);
COleDateTime dt;
if(dt.ParseDateTime(strSubItemText))pDTC->SetTime(dt);
}
break;
}
return TRUE;
}
BOOL CTestList6View::EndEditor(CWnd** pWnd, int nRow, int nColumn,
CString &strSubItemText, DWORD_PTR dwItemData, void* pThis, BOOL bUpdate)
{
ASSERT(pWnd);
switch(nColumn)
{
case 2:
{
CComboBox* pCmb = reinterpret_cast<CComboBox*>(*pWnd);
int index = pCmb->GetCurSel();
if(index >= 0) pCmb->GetLBText(index, strSubItemText);
}
break;
case 3:
{
CDateTimeCtrl* pDTC = reinterpret_cast<CDateTimeCtrl*>(*pWnd);
COleDateTime dt;
pDTC->GetTime(dt);
strSubItemText = dt.Format();
}
break;
}
return TRUE;
}
在这里,你可以在编辑控件的开头(InitEditor(...)
)或结尾(EndEditor(...)
)处理你的自定义操作。
使用 CListCtrlExt
的另一种可能性是将其用作 CListView
控件(你可以在第二个示例项目中看到这一点);在这种情况下,你需要将一个额外的类包含到你的项目中:CChildListCtrlExt
。
在这里,m_List
成为 CChildListCtrlExt
类成员(而不是 CListCtrlExt
);在 CChildListCtrlExt
中将你的视图类声明为 friend
,并在视图类中处理几个事件
BOOL CTestList6View::PreTranslateMessage(MSG* pMsg)
{
// TODO: Add your specialized code here and/or call the base class
if(! CListView::PreTranslateMessage(pMsg))
return m_List.PreTranslateMessage(pMsg);
return FALSE;
}
BOOL CTestList6View::OnChildNotify(UINT message, WPARAM wParam,
LPARAM lParam, LRESULT* pLResult)
{
// TODO: Add your specialized code here and/or call the base class
if(! CListView::OnChildNotify(message, wParam, lParam, pLResult))
return m_List.OnChildNotify(message, wParam, lParam, pLResult);
return FALSE;
}
LRESULT CTestList6View::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
// TODO: Add your specialized code here and/or call the base class
LRESULT lResult = 0;
if(! CListView::OnWndMsg(message, wParam, lParam, &lResult))
{
if(! m_List.OnWndMsg(message, wParam, lParam, &lResult))
{
lResult = DefWindowProc(message, wParam, lParam);
}
}
return lResult;
}
在需要调用 GetListCtrl()
的地方,改为键入 m_List
。这里有一个小观察:要将父消息传递给子控件,你需要反射它们,如下面的代码所示
// TestList6View.h : interface of the CTestList6View class
//
// Generated message map functions
protected:
//{{AFX_MSG(CTestList6View)
afx_msg LRESULT OnColumnclick(NMHDR* pNMHDR, LRESULT* pResult);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
// TestList6View.cpp : implementation of the CTestList6View class
//
BEGIN_MESSAGE_MAP(CTestList6View, CListView)
//{{AFX_MSG_MAP(CTestList6View)
ON_NOTIFY_REFLECT_EX(LVN_COLUMNCLICK, OnColumnclick)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
...
...
LRESULT CTestList6View::OnColumnclick(NMHDR* pNMHDR, LRESULT* pResult)
{
NM_LISTVIEW* phdr = reinterpret_cast<NM_LISTVIEW*>(pNMHDR);
// TODO: Add your control notification handler code here
m_nColumnSort = phdr->iSubItem;
*pResult = 0;
return *pResult;
}
我从 这里 获取了 listview 实现的模型。
挑战
我随着时间的推移打磨了这个类,从中选择了几个文章的特性。我在这里只列出了最后一篇启发我显示如何在一个 CListView
类中控制一个派生 CListCtrl
的文章。几年后,Zafir Anjum 也说过我们不能 在一个 CListView 中使用派生的 CListCtrl。我不会反驳他,他在文章中的说法非常合乎逻辑。不过,第二个示例项目似乎运行良好,而且我已经在几个项目中将 CListCtrlExt
类用作 CListView
控件,并且到目前为止没有遇到任何问题……我将留给你自己去发现这个实现的任何问题。最后,但同样重要的是,我要感谢 codexpert 团队。
CListViewExt 类
CListViewExt
类继承自 CListView
,并具有与 CListCtrlExt
相同的功能。它是如何工作的?视图类,在本例中为 CTestList6View
,我们继承自 CListViewExt
类。如果我们只需要标准的 CListCtrl
方法,我们以正常方式获取 CListCtrl
指针:GetListCtrl()
。如果我们只需要自定义 CListViewExt
方法(类似于 CListCtrlExt
方法),我们获取 CListViewExt
指针:GetListViewExt()
。例如
GetListCtrl().InsertColumn(0, "Integer", LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(1, "String", LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(2, "List", LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(3, "DateTime", LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(4, "Random", LVCFMT_LEFT, 100);
GetListCtrlExt().SetColumnSorting(0, CListViewExt::Auto, CListViewExt::Int);
GetListCtrlExt().SetColumnSorting(1, CListViewExt::Auto, CListViewExt::StringNoCase);
GetListCtrlExt().SetColumnSorting(2, CListViewExt::Auto, CListViewExt::StringNoCase);
GetListCtrlExt().SetColumnSorting(3, CListViewExt::Auto, CListViewExt::Date);
GetListCtrlExt().SetColumnSorting(4, CListViewExt::Auto, CListViewExt::StringNoCase);
但是由于 CTestList6View
类继承自 CListViewExt
类,我们根本不需要获取 GetListCtrlExt()
指针
GetListCtrl().InsertColumn(0, "Integer", LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(1, "String", LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(2, "List", LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(3, "DateTime", LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(4, "Random", LVCFMT_LEFT, 100);
SetColumnSorting(0, CListViewExt::Auto, CListViewExt::Int);
SetColumnSorting(1, CListViewExt::Auto, CListViewExt::StringNoCase);
SetColumnSorting(2, CListViewExt::Auto, CListViewExt::StringNoCase);
SetColumnSorting(3, CListViewExt::Auto, CListViewExt::Date);
SetColumnSorting(4, CListViewExt::Auto, CListViewExt::StringNoCase);
我们所要做的就是在 CTestList6View::OnInitialUpdate()
的基类中调用 CListViewExt::OnInitialUpdate();
方法。
void CTestList6View::OnInitialUpdate()
{
CListViewExt::OnInitialUpdate();
// TODO: You may populate your ListView with items by directly accessing
// its list control through a call to GetListCtrl().
....
....
}
有一个非常重要的注意事项:在 CListViewExt
中,你不能使用标准的 GetItemData()/SetItemData
CListCtrl
方法。请改用自定义的 GetItemUserData()/SetItemUserData()
CListView
Ext 方法!!!请注意这个细节!无论如何,你还可以下载一个演示项目。
历史
- 2011年10月11日:我修改了
PreSubclassWindow
方法,现在列表控件可以在任何样式下启动,并且仍然具有报表设置。 - 2011年10月24日:我上传了
CListViewExt
类,它继承自CListView
类,并具有与CListCtrlExt
类相同的功能。 - 2012年2月20日:添加了
CListCtrlExt::GetFocusCell()
、CListViewExt::GetFocusCell()
来获取焦点单元格的索引。修改了BOOL CListCtrlExt::SaveState(LPCTSTR lpszListName); BOOL CListCtrlExt::RestoreState(LPCTSTR lpszListName);
以在注册表中设置列表视图名称。