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()
CListView
Ext 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.