
Introduction
One of the most important and useful controls in MFC (and not the only one) is the list control. For a long time, I searched for a good one that is fully
MFC compatible, and not finding one, I gathered information from several articles (and sites) and I drafted myself one which:
- Doesn't have all the standard capabilities (
GetItemData, SetItemData, etc.)
- Is fully MFC compatible (is derived from
CListCtrl)
- Can be used in any kind of style (
LVS_ICON, LVS_SMALLICON, LVS_LIST, LVS_REPORT)
- Can sort data (and show the sort direction without any external resources)
- Can color text and/or background of cells, rows, columns
- Has full header control
- Persists column width, order, appearance, and sorting
- Can have grid behaviour
- Can be inserted in cells with various static controls (e.g.,
CEdit, CComboBox, COleDateTime, etc.)
- Can be used like the MFC
CListView standard control (instead of GetListCtrl())
and all that with only three classes (four in CListView's case).
Background
Like I said above, CListCtrlExt is derived from the CListCtrl MFC class. That means you can use it in projects which
use the standard CListCtrl without any modifications. For the new functionality, you need to call custom methods (choose sorting columns, grid
behaviour, etc.). Of course, many of the above features are available only in the LVS_REPORT style. And most importantly, this
CListCtrlExt class can be used like the CListView control. You need one more class to include in your project: CChildCListCtrlExt class.
And one more thing, being a standard control, appearance style (theme of control) can be handled like any other standard control without any complications (extra theme classes).
Using the code
First, you need to include in your project six files (three classes): ListCtrlExt.h, ListCtrlExt.cpp, HeaderCtrlExt.h,
HeaderCtrlExt.cpp, MsgHook.h, and MsgHook.cpp. Let's say you have an SDI application with CMyView
(CTestList6View in the sample project) based on CView. We could create our list in dynamic mode (in the resource header, Resource.h, define):
#define IDC_LIST 1001
and then declare a CListCtrlExt variable:
class CTestList6View : public CView
{
...
...
protected:
CListCtrlExt m_List;
};
int CTestList6View::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if(CView::OnCreate(lpCreateStruct) == -1)return -1;
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;
}
void CTestList6View::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
if(::IsWindow(m_List.m_hWnd))m_List.MoveWindow(0,0,cx,cy,TRUE);
}
In a lot of situations, we'd want to sort data in a list control. No problem, after column inserts, we call SetColumnSorting(...) like:
void CTestList6View::OnInitialUpdate()
{
CView::OnInitialUpdate();
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);
}
To color the text or background of a list, you have the following methods to paint a cell, row, or column: SetCellColors(...),
SetRowColors(...), SetColumnColors(...).
If you use this list control in LVS_REPORT style, you have a full header control: on right click on the header, you can choose which column
to be visible and which not, but this feature is available only if you set at least one column to be irremovable:
m_List.GetHeaderCtrl()->SetRemovable(0,FALSE);
To memorize the column width, column order, which column to be visible, even the last column which was sorted, you have call
two methods: RestoreState(...) after you load the list the very first time, and SaveState(...) in the list destroy handler.
In case you want to have a grid behaviuor (navigate on individual cells, search on any column), you must do two things: set the LVS_EX_FULLROWSELECT style on and call
SetGridBehaviour().
Also, you can insert controls (CEdit, CComboBox, CDateTimeCtrl, etc. ) after you create it in dynamic mode; in this
case, you need to implement two static methods: InitEditor(...) and EndEditor(...).
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;
and here is the implementation code:
void CTestList6View::OnInitialUpdate()
{
CView::OnInitialUpdate();
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;
}
Here you can handle your custom action at the beginning of the edit control (InitEditor(...)) or at the ending of the edit control (EndEditor(...)).
Another possibility to use CListCtrlExt is like the CListView control (you can see this in the second sample project);
in this case, you need to include in your project one more class: CChildListCtrlExt.
Here, m_List becomes the CChildListCtrlExt class member (not CListCtrlExt); declare as friend your view
class in CChildListCtrlExt and handle a few events in the view class:
BOOL CTestList6View::PreTranslateMessage(MSG* pMsg)
{
if(! CListView::PreTranslateMessage(pMsg))
return m_List.PreTranslateMessage(pMsg);
return FALSE;
}
BOOL CTestList6View::OnChildNotify(UINT message, WPARAM wParam,
LPARAM lParam, LRESULT* pLResult)
{
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)
{
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;
}
and where you need to call GetListCtrl(), type m_List instead. A little observation here: to reach parent messages
to child, you need to reflect them, like in the code below:
protected:
afx_msg LRESULT OnColumnclick(NMHDR* pNMHDR, LRESULT* pResult);
DECLARE_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CTestList6View, CListView)
ON_NOTIFY_REFLECT_EX(LVN_COLUMNCLICK, OnColumnclick)
END_MESSAGE_MAP()
...
...
LRESULT CTestList6View::OnColumnclick(NMHDR* pNMHDR, LRESULT* pResult)
{
NM_LISTVIEW* phdr = reinterpret_cast<NM_LISTVIEW*>(pNMHDR);
m_nColumnSort = phdr->iSubItem;
*pResult = 0;
return *pResult;
}
I got the model for the listview implementation from here.
The challenge
I polished this class over time, picking features from several articles. I listed here only the last article from where I was inspired that show the way a
derived CListCtrl can be controlled in a CListView class. The same Zafir Anjum said after few years that we can not
use a derived CListCtrl with a CListView. I will not contradict him, what he
says in his article is very logical. Still, the second sample project seems to work well and I have used the CListCtrlExt class like the
CListView control in a few projects by now without problems... I will let you discover any problems of this implementation. Last, but not the
least, I want to thank the codexpert team.
CListViewExt class
The CListViewExt class is derived from CListView and has the same functionality as CListCtrlExt. How does it function? The view class,
CTestList6View in our case, we derive from the CListViewExt class. If we need standard CListCtrl methods, we get a CListCtrl
pointer the normal way: GetListCtrl(). If we need custom CListViewExt methods (similar to CListCtrlExt methods), we get
a CListViewExt pointer: GetListViewExt(). For example:
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);
but as the CTestList6View class is derived from the CListViewExt class, we don't need to get the GetListCtrlExt() pointer at all:
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);
All we have to do is call the CListViewExt::OnInitialUpdate(); method in the CTestList6View::OnInitialUpdate() base.
void CTestList6View::OnInitialUpdate()
{
CListViewExt::OnInitialUpdate();
....
....
}
There is one very important note: in CListViewExt, you can not use the standard GetItemData()/SetItemData CListCtrl methods.
Use the custom GetItemUserData()/SetItemUserData() CListViewExt methods instead !!! Be aware of this detail! Anyway, you have a demo project attached.
History
- 11 Oct. 2011: I changed the
PreSubclassWindow method in such a way that now the list control can be started in any style and will still have report settings.
- 24 Oct. 2011: I uploaded the
CListViewExt class, derived from the CListView class and with same functionality like the CListCtrlExt class.
- 20 Feb. 2012: Added
CListCtrlExt::GetFocusCell(), CListViewExt::GetFocusCell() to get index of focused cell. Modified BOOL CListCtrlExt::SaveState(LPCTSTR lpszListName); BOOL CListCtrlExt::RestoreState(LPCTSTR lpszListName); to setup a listview name in Registry.